@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,124 @@
|
|
|
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
|
+
import chalk from 'chalk';
|
|
8
|
+
import { DEFAULT_DAEMON_PORT } from '../constants.js';
|
|
9
|
+
const DAEMON_PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
|
|
10
|
+
const DAEMON_URL = `http://127.0.0.1:${DAEMON_PORT}`;
|
|
11
|
+
async function fetchStatus() {
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
const timer = setTimeout(() => controller.abort(), 2000);
|
|
14
|
+
try {
|
|
15
|
+
const res = await fetch(`${DAEMON_URL}/status`, {
|
|
16
|
+
headers: { 'X-OpenCLI': '1' },
|
|
17
|
+
signal: controller.signal,
|
|
18
|
+
});
|
|
19
|
+
if (!res.ok)
|
|
20
|
+
return null;
|
|
21
|
+
return await res.json();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function requestShutdown() {
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
const timer = setTimeout(() => controller.abort(), 5000);
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch(`${DAEMON_URL}/shutdown`, {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: { 'X-OpenCLI': '1' },
|
|
37
|
+
signal: controller.signal,
|
|
38
|
+
});
|
|
39
|
+
return res.ok;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function formatUptime(seconds) {
|
|
49
|
+
const h = Math.floor(seconds / 3600);
|
|
50
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
51
|
+
if (h > 0)
|
|
52
|
+
return `${h}h ${m}m`;
|
|
53
|
+
if (m > 0)
|
|
54
|
+
return `${m}m`;
|
|
55
|
+
return `${Math.floor(seconds)}s`;
|
|
56
|
+
}
|
|
57
|
+
function formatTimeSince(timestampMs) {
|
|
58
|
+
const seconds = (Date.now() - timestampMs) / 1000;
|
|
59
|
+
if (seconds < 60)
|
|
60
|
+
return `${Math.floor(seconds)}s ago`;
|
|
61
|
+
const m = Math.floor(seconds / 60);
|
|
62
|
+
if (m < 60)
|
|
63
|
+
return `${m} min ago`;
|
|
64
|
+
const h = Math.floor(m / 60);
|
|
65
|
+
return `${h}h ${m % 60}m ago`;
|
|
66
|
+
}
|
|
67
|
+
export async function daemonStatus() {
|
|
68
|
+
const status = await fetchStatus();
|
|
69
|
+
if (!status) {
|
|
70
|
+
console.log(`Daemon: ${chalk.dim('not running')}`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
console.log(`Daemon: ${chalk.green('running')} (PID ${status.pid})`);
|
|
74
|
+
console.log(`Uptime: ${formatUptime(status.uptime)}`);
|
|
75
|
+
console.log(`Extension: ${status.extensionConnected ? chalk.green('connected') : chalk.yellow('disconnected')}`);
|
|
76
|
+
console.log(`Last CLI request: ${formatTimeSince(status.lastCliRequestTime)}`);
|
|
77
|
+
console.log(`Memory: ${status.memoryMB} MB`);
|
|
78
|
+
console.log(`Port: ${status.port}`);
|
|
79
|
+
}
|
|
80
|
+
export async function daemonStop() {
|
|
81
|
+
const status = await fetchStatus();
|
|
82
|
+
if (!status) {
|
|
83
|
+
console.log(chalk.dim('Daemon is not running.'));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const ok = await requestShutdown();
|
|
87
|
+
if (ok) {
|
|
88
|
+
console.log(chalk.green('Daemon stopped.'));
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
console.error(chalk.red('Failed to stop daemon.'));
|
|
92
|
+
process.exitCode = 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export async function daemonRestart() {
|
|
96
|
+
const status = await fetchStatus();
|
|
97
|
+
if (status) {
|
|
98
|
+
const ok = await requestShutdown();
|
|
99
|
+
if (!ok) {
|
|
100
|
+
console.error(chalk.red('Failed to stop daemon.'));
|
|
101
|
+
process.exitCode = 1;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// Wait for daemon to actually exit (poll until unreachable)
|
|
105
|
+
const deadline = Date.now() + 5000;
|
|
106
|
+
while (Date.now() < deadline) {
|
|
107
|
+
await new Promise(r => setTimeout(r, 200));
|
|
108
|
+
if (!(await fetchStatus()))
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Import BrowserBridge to spawn a new daemon
|
|
113
|
+
const { BrowserBridge } = await import('../browser/mcp.js');
|
|
114
|
+
const bridge = new BrowserBridge();
|
|
115
|
+
try {
|
|
116
|
+
console.log('Starting daemon...');
|
|
117
|
+
await bridge.connect({ timeout: 10 });
|
|
118
|
+
console.log(chalk.green('Daemon restarted.'));
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
console.error(chalk.red(`Failed to restart daemon: ${err instanceof Error ? err.message : err}`));
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
vi.mock('chalk', () => ({
|
|
3
|
+
default: {
|
|
4
|
+
green: (s) => s,
|
|
5
|
+
yellow: (s) => s,
|
|
6
|
+
red: (s) => s,
|
|
7
|
+
dim: (s) => s,
|
|
8
|
+
},
|
|
9
|
+
}));
|
|
10
|
+
const mockConnect = vi.fn();
|
|
11
|
+
vi.mock('../browser/mcp.js', () => ({
|
|
12
|
+
BrowserBridge: class {
|
|
13
|
+
connect = mockConnect;
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
import { daemonStatus, daemonStop, daemonRestart } from './daemon.js';
|
|
17
|
+
describe('daemon commands', () => {
|
|
18
|
+
let logSpy;
|
|
19
|
+
let errorSpy;
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
22
|
+
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
vi.restoreAllMocks();
|
|
26
|
+
mockConnect.mockReset();
|
|
27
|
+
});
|
|
28
|
+
describe('daemonStatus', () => {
|
|
29
|
+
it('shows "not running" when daemon is unreachable', async () => {
|
|
30
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('ECONNREFUSED')));
|
|
31
|
+
await daemonStatus();
|
|
32
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('not running'));
|
|
33
|
+
});
|
|
34
|
+
it('shows "not running" when daemon returns non-ok response', async () => {
|
|
35
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false }));
|
|
36
|
+
await daemonStatus();
|
|
37
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('not running'));
|
|
38
|
+
});
|
|
39
|
+
it('shows daemon info when running', async () => {
|
|
40
|
+
const status = {
|
|
41
|
+
ok: true,
|
|
42
|
+
pid: 12345,
|
|
43
|
+
uptime: 3661,
|
|
44
|
+
extensionConnected: true,
|
|
45
|
+
pending: 0,
|
|
46
|
+
lastCliRequestTime: Date.now() - 30_000,
|
|
47
|
+
memoryMB: 64,
|
|
48
|
+
port: 19825,
|
|
49
|
+
};
|
|
50
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
51
|
+
ok: true,
|
|
52
|
+
json: () => Promise.resolve(status),
|
|
53
|
+
}));
|
|
54
|
+
await daemonStatus();
|
|
55
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('running'));
|
|
56
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('PID 12345'));
|
|
57
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('1h 1m'));
|
|
58
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('connected'));
|
|
59
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('64 MB'));
|
|
60
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('19825'));
|
|
61
|
+
});
|
|
62
|
+
it('shows disconnected when extension is not connected', async () => {
|
|
63
|
+
const status = {
|
|
64
|
+
ok: true,
|
|
65
|
+
pid: 99,
|
|
66
|
+
uptime: 120,
|
|
67
|
+
extensionConnected: false,
|
|
68
|
+
pending: 0,
|
|
69
|
+
lastCliRequestTime: Date.now() - 5000,
|
|
70
|
+
memoryMB: 32,
|
|
71
|
+
port: 19825,
|
|
72
|
+
};
|
|
73
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
74
|
+
ok: true,
|
|
75
|
+
json: () => Promise.resolve(status),
|
|
76
|
+
}));
|
|
77
|
+
await daemonStatus();
|
|
78
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('disconnected'));
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe('daemonStop', () => {
|
|
82
|
+
it('reports "not running" when daemon is unreachable', async () => {
|
|
83
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('ECONNREFUSED')));
|
|
84
|
+
await daemonStop();
|
|
85
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('not running'));
|
|
86
|
+
});
|
|
87
|
+
it('sends shutdown and reports success', async () => {
|
|
88
|
+
const statusResponse = {
|
|
89
|
+
ok: true,
|
|
90
|
+
json: () => Promise.resolve({
|
|
91
|
+
ok: true,
|
|
92
|
+
pid: 12345,
|
|
93
|
+
uptime: 100,
|
|
94
|
+
extensionConnected: true,
|
|
95
|
+
pending: 0,
|
|
96
|
+
lastCliRequestTime: Date.now(),
|
|
97
|
+
memoryMB: 50,
|
|
98
|
+
port: 19825,
|
|
99
|
+
}),
|
|
100
|
+
};
|
|
101
|
+
const shutdownResponse = { ok: true };
|
|
102
|
+
const mockFetch = vi.fn()
|
|
103
|
+
.mockResolvedValueOnce(statusResponse)
|
|
104
|
+
.mockResolvedValueOnce(shutdownResponse);
|
|
105
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
106
|
+
await daemonStop();
|
|
107
|
+
// Verify shutdown was called with POST
|
|
108
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
109
|
+
const shutdownCall = mockFetch.mock.calls[1];
|
|
110
|
+
expect(shutdownCall[0]).toContain('/shutdown');
|
|
111
|
+
expect(shutdownCall[1]).toMatchObject({ method: 'POST' });
|
|
112
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Daemon stopped'));
|
|
113
|
+
});
|
|
114
|
+
it('reports failure when shutdown request fails', async () => {
|
|
115
|
+
const statusResponse = {
|
|
116
|
+
ok: true,
|
|
117
|
+
json: () => Promise.resolve({
|
|
118
|
+
ok: true,
|
|
119
|
+
pid: 12345,
|
|
120
|
+
uptime: 100,
|
|
121
|
+
extensionConnected: true,
|
|
122
|
+
pending: 0,
|
|
123
|
+
lastCliRequestTime: Date.now(),
|
|
124
|
+
memoryMB: 50,
|
|
125
|
+
port: 19825,
|
|
126
|
+
}),
|
|
127
|
+
};
|
|
128
|
+
const shutdownResponse = { ok: false };
|
|
129
|
+
const mockFetch = vi.fn()
|
|
130
|
+
.mockResolvedValueOnce(statusResponse)
|
|
131
|
+
.mockResolvedValueOnce(shutdownResponse);
|
|
132
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
133
|
+
await daemonStop();
|
|
134
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to stop daemon'));
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
describe('daemonRestart', () => {
|
|
138
|
+
const statusData = {
|
|
139
|
+
ok: true,
|
|
140
|
+
pid: 12345,
|
|
141
|
+
uptime: 100,
|
|
142
|
+
extensionConnected: true,
|
|
143
|
+
pending: 0,
|
|
144
|
+
lastCliRequestTime: Date.now(),
|
|
145
|
+
memoryMB: 50,
|
|
146
|
+
port: 19825,
|
|
147
|
+
};
|
|
148
|
+
it('starts daemon directly when not running', async () => {
|
|
149
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('ECONNREFUSED')));
|
|
150
|
+
mockConnect.mockResolvedValue(undefined);
|
|
151
|
+
await daemonRestart();
|
|
152
|
+
expect(mockConnect).toHaveBeenCalledWith({ timeout: 10 });
|
|
153
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Daemon restarted'));
|
|
154
|
+
});
|
|
155
|
+
it('stops then starts when daemon is running', async () => {
|
|
156
|
+
const mockFetch = vi.fn()
|
|
157
|
+
// First call: fetchStatus in daemonRestart — daemon is running
|
|
158
|
+
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(statusData) })
|
|
159
|
+
// Second call: requestShutdown — success
|
|
160
|
+
.mockResolvedValueOnce({ ok: true })
|
|
161
|
+
// Subsequent calls: polling fetchStatus until unreachable
|
|
162
|
+
.mockRejectedValue(new Error('ECONNREFUSED'));
|
|
163
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
164
|
+
mockConnect.mockResolvedValue(undefined);
|
|
165
|
+
await daemonRestart();
|
|
166
|
+
// Verify shutdown was called
|
|
167
|
+
const shutdownCall = mockFetch.mock.calls[1];
|
|
168
|
+
expect(shutdownCall[0]).toContain('/shutdown');
|
|
169
|
+
expect(shutdownCall[1]).toMatchObject({ method: 'POST' });
|
|
170
|
+
expect(mockConnect).toHaveBeenCalledWith({ timeout: 10 });
|
|
171
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Daemon restarted'));
|
|
172
|
+
});
|
|
173
|
+
it('aborts when shutdown fails', async () => {
|
|
174
|
+
const mockFetch = vi.fn()
|
|
175
|
+
// fetchStatus — daemon is running
|
|
176
|
+
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(statusData) })
|
|
177
|
+
// requestShutdown — failure
|
|
178
|
+
.mockResolvedValueOnce({ ok: false });
|
|
179
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
180
|
+
await daemonRestart();
|
|
181
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to stop daemon'));
|
|
182
|
+
expect(mockConnect).not.toHaveBeenCalled();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
package/dist/completion.js
CHANGED
|
@@ -50,9 +50,11 @@ export function getCompletions(words, cursor) {
|
|
|
50
50
|
for (const [, cmd] of getRegistry()) {
|
|
51
51
|
if (cmd.site === site) {
|
|
52
52
|
subcommands.push(cmd.name);
|
|
53
|
+
if (cmd.aliases?.length)
|
|
54
|
+
subcommands.push(...cmd.aliases);
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
|
-
return subcommands.sort();
|
|
57
|
+
return [...new Set(subcommands)].sort();
|
|
56
58
|
}
|
|
57
59
|
// cursor >= 3 → no further completion
|
|
58
60
|
return [];
|
package/dist/constants.d.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
/** Default daemon port for HTTP/WebSocket communication with browser extension */
|
|
5
5
|
export declare const DEFAULT_DAEMON_PORT = 19825;
|
|
6
|
+
/** Default idle timeout before daemon auto-exits (ms). Override via OPENCLI_DAEMON_TIMEOUT env var. */
|
|
7
|
+
export declare const DEFAULT_DAEMON_IDLE_TIMEOUT: number;
|
|
6
8
|
/** URL query params that are volatile/ephemeral and should be stripped from patterns */
|
|
7
9
|
export declare const VOLATILE_PARAMS: Set<string>;
|
|
8
10
|
/** Search-related query parameter names */
|
package/dist/constants.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
/** Default daemon port for HTTP/WebSocket communication with browser extension */
|
|
5
5
|
export const DEFAULT_DAEMON_PORT = 19825;
|
|
6
|
+
/** Default idle timeout before daemon auto-exits (ms). Override via OPENCLI_DAEMON_TIMEOUT env var. */
|
|
7
|
+
export const DEFAULT_DAEMON_IDLE_TIMEOUT = 4 * 60 * 60 * 1000; // 4 hours
|
|
6
8
|
/** URL query params that are volatile/ephemeral and should be stripped from patterns */
|
|
7
9
|
export const VOLATILE_PARAMS = new Set([
|
|
8
10
|
'w_rid', 'wts', '_', 'callback', 'timestamp', 't', 'nonce', 'sign',
|
package/dist/daemon.d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
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
|
export {};
|
package/dist/daemon.js
CHANGED
|
@@ -15,20 +15,20 @@
|
|
|
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
|
import { createServer } from 'node:http';
|
|
22
22
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
23
|
-
import { DEFAULT_DAEMON_PORT } from './constants.js';
|
|
23
|
+
import { DEFAULT_DAEMON_PORT, DEFAULT_DAEMON_IDLE_TIMEOUT } from './constants.js';
|
|
24
24
|
import { EXIT_CODES } from './errors.js';
|
|
25
|
+
import { IdleManager } from './idle-manager.js';
|
|
25
26
|
const PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
|
|
26
|
-
const IDLE_TIMEOUT =
|
|
27
|
+
const IDLE_TIMEOUT = Number(process.env.OPENCLI_DAEMON_TIMEOUT ?? DEFAULT_DAEMON_IDLE_TIMEOUT);
|
|
27
28
|
// ─── State ───────────────────────────────────────────────────────────
|
|
28
29
|
let extensionWs = null;
|
|
29
30
|
let extensionVersion = null;
|
|
30
31
|
const pending = new Map();
|
|
31
|
-
let idleTimer = null;
|
|
32
32
|
const LOG_BUFFER_SIZE = 200;
|
|
33
33
|
const logBuffer = [];
|
|
34
34
|
function pushLog(entry) {
|
|
@@ -37,14 +37,10 @@ function pushLog(entry) {
|
|
|
37
37
|
logBuffer.shift();
|
|
38
38
|
}
|
|
39
39
|
// ─── Idle auto-exit ──────────────────────────────────────────────────
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
console.error('[daemon] Idle timeout, shutting down');
|
|
45
|
-
process.exit(EXIT_CODES.SUCCESS);
|
|
46
|
-
}, IDLE_TIMEOUT);
|
|
47
|
-
}
|
|
40
|
+
const idleManager = new IdleManager(IDLE_TIMEOUT, () => {
|
|
41
|
+
console.error('[daemon] Idle timeout (no CLI requests + no Extension), shutting down');
|
|
42
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
43
|
+
});
|
|
48
44
|
// ─── HTTP Server ─────────────────────────────────────────────────────
|
|
49
45
|
const MAX_BODY = 1024 * 1024; // 1 MB — commands are tiny; this prevents OOM
|
|
50
46
|
function readBody(req) {
|
|
@@ -113,11 +109,18 @@ async function handleRequest(req, res) {
|
|
|
113
109
|
return;
|
|
114
110
|
}
|
|
115
111
|
if (req.method === 'GET' && pathname === '/status') {
|
|
112
|
+
const uptime = process.uptime();
|
|
113
|
+
const mem = process.memoryUsage();
|
|
116
114
|
jsonResponse(res, 200, {
|
|
117
115
|
ok: true,
|
|
116
|
+
pid: process.pid,
|
|
117
|
+
uptime,
|
|
118
118
|
extensionConnected: extensionWs?.readyState === WebSocket.OPEN,
|
|
119
119
|
extensionVersion,
|
|
120
120
|
pending: pending.size,
|
|
121
|
+
lastCliRequestTime: idleManager.lastCliRequestTime,
|
|
122
|
+
memoryMB: Math.round(mem.rss / 1024 / 1024 * 10) / 10,
|
|
123
|
+
port: PORT,
|
|
121
124
|
});
|
|
122
125
|
return;
|
|
123
126
|
}
|
|
@@ -135,8 +138,13 @@ async function handleRequest(req, res) {
|
|
|
135
138
|
jsonResponse(res, 200, { ok: true });
|
|
136
139
|
return;
|
|
137
140
|
}
|
|
141
|
+
if (req.method === 'POST' && pathname === '/shutdown') {
|
|
142
|
+
jsonResponse(res, 200, { ok: true, message: 'Shutting down' });
|
|
143
|
+
setTimeout(() => shutdown(), 100);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
138
146
|
if (req.method === 'POST' && url === '/command') {
|
|
139
|
-
|
|
147
|
+
idleManager.onCliRequest();
|
|
140
148
|
try {
|
|
141
149
|
const body = JSON.parse(await readBody(req));
|
|
142
150
|
if (!body.id) {
|
|
@@ -188,6 +196,7 @@ wss.on('connection', (ws) => {
|
|
|
188
196
|
console.error('[daemon] Extension connected');
|
|
189
197
|
extensionWs = ws;
|
|
190
198
|
extensionVersion = null; // cleared until hello message arrives
|
|
199
|
+
idleManager.setExtensionConnected(true);
|
|
191
200
|
// ── Heartbeat: ping every 15s, close if 2 pongs missed ──
|
|
192
201
|
let missedPongs = 0;
|
|
193
202
|
const heartbeatInterval = setInterval(() => {
|
|
@@ -240,6 +249,7 @@ wss.on('connection', (ws) => {
|
|
|
240
249
|
if (extensionWs === ws) {
|
|
241
250
|
extensionWs = null;
|
|
242
251
|
extensionVersion = null;
|
|
252
|
+
idleManager.setExtensionConnected(false);
|
|
243
253
|
// Reject all pending requests since the extension is gone
|
|
244
254
|
for (const [id, p] of pending) {
|
|
245
255
|
clearTimeout(p.timer);
|
|
@@ -253,6 +263,7 @@ wss.on('connection', (ws) => {
|
|
|
253
263
|
if (extensionWs === ws) {
|
|
254
264
|
extensionWs = null;
|
|
255
265
|
extensionVersion = null;
|
|
266
|
+
idleManager.setExtensionConnected(false);
|
|
256
267
|
// Reject pending requests in case 'close' does not follow this 'error'
|
|
257
268
|
for (const [, p] of pending) {
|
|
258
269
|
clearTimeout(p.timer);
|
|
@@ -265,7 +276,7 @@ wss.on('connection', (ws) => {
|
|
|
265
276
|
// ─── Start ───────────────────────────────────────────────────────────
|
|
266
277
|
httpServer.listen(PORT, '127.0.0.1', () => {
|
|
267
278
|
console.error(`[daemon] Listening on http://127.0.0.1:${PORT}`);
|
|
268
|
-
|
|
279
|
+
idleManager.onCliRequest();
|
|
269
280
|
});
|
|
270
281
|
httpServer.on('error', (err) => {
|
|
271
282
|
if (err.code === 'EADDRINUSE') {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { IdleManager } from './idle-manager.js';
|
|
3
|
+
describe('IdleManager', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
vi.useFakeTimers();
|
|
6
|
+
});
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
vi.useRealTimers();
|
|
9
|
+
});
|
|
10
|
+
it('does not start timer when extension is connected', () => {
|
|
11
|
+
const exit = vi.fn();
|
|
12
|
+
const mgr = new IdleManager(300_000, exit);
|
|
13
|
+
mgr.setExtensionConnected(true);
|
|
14
|
+
mgr.onCliRequest();
|
|
15
|
+
vi.advanceTimersByTime(300_000 + 1000);
|
|
16
|
+
expect(exit).not.toHaveBeenCalled();
|
|
17
|
+
});
|
|
18
|
+
it('starts timer when extension disconnects and CLI is idle', () => {
|
|
19
|
+
const exit = vi.fn();
|
|
20
|
+
const mgr = new IdleManager(300_000, exit);
|
|
21
|
+
mgr.onCliRequest();
|
|
22
|
+
mgr.setExtensionConnected(true);
|
|
23
|
+
mgr.setExtensionConnected(false);
|
|
24
|
+
expect(exit).not.toHaveBeenCalled();
|
|
25
|
+
vi.advanceTimersByTime(300_000 + 1000);
|
|
26
|
+
expect(exit).toHaveBeenCalledTimes(1);
|
|
27
|
+
});
|
|
28
|
+
it('exits immediately on extension disconnect if CLI has been idle past timeout', () => {
|
|
29
|
+
const exit = vi.fn();
|
|
30
|
+
const mgr = new IdleManager(300_000, exit);
|
|
31
|
+
mgr.onCliRequest();
|
|
32
|
+
mgr.setExtensionConnected(true); // connect before timeout elapses
|
|
33
|
+
vi.advanceTimersByTime(400_000); // CLI idle time exceeds timeout, but extension is connected so no exit
|
|
34
|
+
expect(exit).not.toHaveBeenCalled();
|
|
35
|
+
mgr.setExtensionConnected(false); // disconnect → should exit immediately since CLI idle > timeout
|
|
36
|
+
expect(exit).toHaveBeenCalledTimes(1);
|
|
37
|
+
});
|
|
38
|
+
it('resets timer on new CLI request', () => {
|
|
39
|
+
const exit = vi.fn();
|
|
40
|
+
const mgr = new IdleManager(300_000, exit);
|
|
41
|
+
mgr.onCliRequest();
|
|
42
|
+
vi.advanceTimersByTime(200_000);
|
|
43
|
+
mgr.onCliRequest();
|
|
44
|
+
vi.advanceTimersByTime(200_000);
|
|
45
|
+
expect(exit).not.toHaveBeenCalled();
|
|
46
|
+
vi.advanceTimersByTime(100_001);
|
|
47
|
+
expect(exit).toHaveBeenCalledTimes(1);
|
|
48
|
+
});
|
|
49
|
+
it('does not exit when timeout is 0 (disabled)', () => {
|
|
50
|
+
const exit = vi.fn();
|
|
51
|
+
const mgr = new IdleManager(0, exit);
|
|
52
|
+
mgr.onCliRequest();
|
|
53
|
+
vi.advanceTimersByTime(24 * 60 * 60 * 1000);
|
|
54
|
+
expect(exit).not.toHaveBeenCalled();
|
|
55
|
+
});
|
|
56
|
+
it('clears timer when extension connects', () => {
|
|
57
|
+
const exit = vi.fn();
|
|
58
|
+
const mgr = new IdleManager(300_000, exit);
|
|
59
|
+
mgr.onCliRequest();
|
|
60
|
+
vi.advanceTimersByTime(200_000);
|
|
61
|
+
mgr.setExtensionConnected(true);
|
|
62
|
+
vi.advanceTimersByTime(200_000);
|
|
63
|
+
expect(exit).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
});
|
package/dist/discovery.d.ts
CHANGED
|
@@ -7,8 +7,17 @@
|
|
|
7
7
|
* TS modules are loaded lazily only when their command is executed.
|
|
8
8
|
* 2. FALLBACK (filesystem scan): Traditional runtime discovery for development.
|
|
9
9
|
*/
|
|
10
|
+
/** User runtime directory: ~/.opencli */
|
|
11
|
+
export declare const USER_OPENCLI_DIR: string;
|
|
12
|
+
/** User CLIs directory: ~/.opencli/clis */
|
|
13
|
+
export declare const USER_CLIS_DIR: string;
|
|
10
14
|
/** Plugins directory: ~/.opencli/plugins/ */
|
|
11
15
|
export declare const PLUGINS_DIR: string;
|
|
16
|
+
/**
|
|
17
|
+
* Create runtime shim files under ~/.opencli so legacy user TS CLIs can keep
|
|
18
|
+
* importing ../../registry(.js) and ../../errors(.js).
|
|
19
|
+
*/
|
|
20
|
+
export declare function ensureUserCliCompatShims(baseDir?: string): Promise<void>;
|
|
12
21
|
/**
|
|
13
22
|
* Discover and register CLI commands.
|
|
14
23
|
* Uses pre-compiled manifest when available for instant startup.
|
package/dist/discovery.js
CHANGED
|
@@ -10,13 +10,17 @@
|
|
|
10
10
|
import * as fs from 'node:fs';
|
|
11
11
|
import * as os from 'node:os';
|
|
12
12
|
import * as path from 'node:path';
|
|
13
|
-
import { pathToFileURL } from 'node:url';
|
|
13
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
14
14
|
import yaml from 'js-yaml';
|
|
15
15
|
import { Strategy, registerCommand } from './registry.js';
|
|
16
16
|
import { getErrorMessage } from './errors.js';
|
|
17
17
|
import { log } from './logger.js';
|
|
18
|
+
/** User runtime directory: ~/.opencli */
|
|
19
|
+
export const USER_OPENCLI_DIR = path.join(os.homedir(), '.opencli');
|
|
20
|
+
/** User CLIs directory: ~/.opencli/clis */
|
|
21
|
+
export const USER_CLIS_DIR = path.join(USER_OPENCLI_DIR, 'clis');
|
|
18
22
|
/** Plugins directory: ~/.opencli/plugins/ */
|
|
19
|
-
export const PLUGINS_DIR = path.join(
|
|
23
|
+
export const PLUGINS_DIR = path.join(USER_OPENCLI_DIR, 'plugins');
|
|
20
24
|
/** Matches files that register commands via cli() or lifecycle hooks */
|
|
21
25
|
const PLUGIN_MODULE_PATTERN = /\b(?:cli|onStartup|onBeforeExecute|onAfterExecute)\s*\(/;
|
|
22
26
|
import { parseYamlArgs } from './yaml-schema.js';
|
|
@@ -27,6 +31,42 @@ function parseStrategy(rawStrategy, fallback = Strategy.COOKIE) {
|
|
|
27
31
|
return Strategy[key] ?? fallback;
|
|
28
32
|
}
|
|
29
33
|
import { isRecord } from './utils.js';
|
|
34
|
+
function resolveHostRuntimeModulePath(moduleName) {
|
|
35
|
+
const runtimeDir = path.dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
for (const ext of ['.js', '.ts']) {
|
|
37
|
+
const candidate = path.join(runtimeDir, `${moduleName}${ext}`);
|
|
38
|
+
if (fs.existsSync(candidate))
|
|
39
|
+
return candidate;
|
|
40
|
+
}
|
|
41
|
+
return path.join(runtimeDir, `${moduleName}.js`);
|
|
42
|
+
}
|
|
43
|
+
async function writeCompatShimIfNeeded(filePath, content) {
|
|
44
|
+
try {
|
|
45
|
+
const existing = await fs.promises.readFile(filePath, 'utf-8');
|
|
46
|
+
if (existing === content)
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Fall through to write missing shim
|
|
51
|
+
}
|
|
52
|
+
await fs.promises.writeFile(filePath, content, 'utf-8');
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Create runtime shim files under ~/.opencli so legacy user TS CLIs can keep
|
|
56
|
+
* importing ../../registry(.js) and ../../errors(.js).
|
|
57
|
+
*/
|
|
58
|
+
export async function ensureUserCliCompatShims(baseDir = USER_OPENCLI_DIR) {
|
|
59
|
+
await fs.promises.mkdir(baseDir, { recursive: true });
|
|
60
|
+
const registryUrl = pathToFileURL(resolveHostRuntimeModulePath('registry-api')).href;
|
|
61
|
+
const errorsUrl = pathToFileURL(resolveHostRuntimeModulePath('errors')).href;
|
|
62
|
+
await Promise.all([
|
|
63
|
+
writeCompatShimIfNeeded(path.join(baseDir, 'registry'), `export * from '${registryUrl}';\n`),
|
|
64
|
+
writeCompatShimIfNeeded(path.join(baseDir, 'registry.js'), `export * from '${registryUrl}';\n`),
|
|
65
|
+
writeCompatShimIfNeeded(path.join(baseDir, 'errors'), `export * from '${errorsUrl}';\n`),
|
|
66
|
+
writeCompatShimIfNeeded(path.join(baseDir, 'errors.js'), `export * from '${errorsUrl}';\n`),
|
|
67
|
+
writeCompatShimIfNeeded(path.join(baseDir, 'package.json'), `${JSON.stringify({ name: 'opencli-user-runtime', private: true, type: 'module' }, null, 2)}\n`),
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
30
70
|
/**
|
|
31
71
|
* Discover and register CLI commands.
|
|
32
72
|
* Uses pre-compiled manifest when available for instant startup.
|
|
@@ -63,6 +103,7 @@ async function loadFromManifest(manifestPath, clisDir) {
|
|
|
63
103
|
const cmd = {
|
|
64
104
|
site: entry.site,
|
|
65
105
|
name: entry.name,
|
|
106
|
+
aliases: entry.aliases,
|
|
66
107
|
description: entry.description ?? '',
|
|
67
108
|
domain: entry.domain,
|
|
68
109
|
strategy,
|
|
@@ -86,6 +127,7 @@ async function loadFromManifest(manifestPath, clisDir) {
|
|
|
86
127
|
const cmd = {
|
|
87
128
|
site: entry.site,
|
|
88
129
|
name: entry.name,
|
|
130
|
+
aliases: entry.aliases,
|
|
89
131
|
description: entry.description ?? '',
|
|
90
132
|
domain: entry.domain,
|
|
91
133
|
strategy,
|
|
@@ -160,6 +202,9 @@ async function registerYamlCli(filePath, defaultSite) {
|
|
|
160
202
|
const cmd = {
|
|
161
203
|
site,
|
|
162
204
|
name,
|
|
205
|
+
aliases: isRecord(cliDef) && Array.isArray(cliDef.aliases)
|
|
206
|
+
? cliDef.aliases.filter((value) => typeof value === 'string')
|
|
207
|
+
: undefined,
|
|
163
208
|
description: cliDef.description ?? '',
|
|
164
209
|
domain: cliDef.domain,
|
|
165
210
|
strategy,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Electron app registry — maps site names to launch metadata.
|
|
3
|
+
*
|
|
4
|
+
* Builtin apps are defined here. User-defined apps are loaded
|
|
5
|
+
* from ~/.opencli/apps.yaml (additive only, does not override builtins).
|
|
6
|
+
*/
|
|
7
|
+
export interface ElectronAppEntry {
|
|
8
|
+
/** CDP debug port (unique per app) */
|
|
9
|
+
port: number;
|
|
10
|
+
/** macOS process name for detection via pgrep */
|
|
11
|
+
processName: string;
|
|
12
|
+
/** macOS bundle ID for path discovery */
|
|
13
|
+
bundleId?: string;
|
|
14
|
+
/** Human-readable name for prompts */
|
|
15
|
+
displayName?: string;
|
|
16
|
+
/** Additional launch args beyond --remote-debugging-port */
|
|
17
|
+
extraArgs?: string[];
|
|
18
|
+
}
|
|
19
|
+
export declare const builtinApps: Record<string, ElectronAppEntry>;
|
|
20
|
+
/** Merge builtin + user-defined apps. User entries are additive only. */
|
|
21
|
+
export declare function loadApps(userApps?: Record<string, Omit<ElectronAppEntry, 'displayName'> & {
|
|
22
|
+
displayName?: string;
|
|
23
|
+
}>): Record<string, ElectronAppEntry>;
|
|
24
|
+
export declare function getElectronApp(site: string): ElectronAppEntry | undefined;
|
|
25
|
+
export declare function isElectronApp(site: string): boolean;
|
|
26
|
+
/** Get all registered apps (builtin + user-defined). */
|
|
27
|
+
export declare function getAllElectronApps(): Record<string, ElectronAppEntry>;
|
|
28
|
+
/** Reset loaded apps (for testing). */
|
|
29
|
+
export declare function _resetRegistry(): void;
|