@jackwener/opencli 1.7.8 → 1.7.10
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 +49 -14
- package/README.zh-CN.md +30 -10
- package/cli-manifest.json +646 -30
- package/clis/36kr/news.js +1 -1
- package/clis/apple-podcasts/commands.test.js +4 -4
- package/clis/apple-podcasts/episodes.js +1 -1
- package/clis/apple-podcasts/search.js +1 -1
- package/clis/apple-podcasts/top.js +1 -1
- package/clis/arxiv/paper.js +1 -1
- package/clis/arxiv/search.js +1 -1
- package/clis/band/mentions.js +3 -3
- package/clis/bbc/news.js +1 -1
- package/clis/bilibili/subtitle.js +2 -2
- package/clis/bloomberg/businessweek.js +1 -1
- package/clis/bloomberg/economics.js +1 -1
- package/clis/bloomberg/industries.js +1 -1
- package/clis/bloomberg/main.js +1 -1
- package/clis/bloomberg/markets.js +1 -1
- package/clis/bloomberg/opinions.js +1 -1
- package/clis/bloomberg/politics.js +1 -1
- package/clis/bloomberg/tech.js +1 -1
- package/clis/boss/search.js +49 -8
- package/clis/boss/search.test.js +78 -0
- package/clis/boss/send.js +3 -3
- package/clis/chatgpt/image.js +37 -8
- package/clis/chatgpt/image.test.js +92 -0
- package/clis/chatgpt/utils.js +39 -6
- package/clis/chatgpt/utils.test.js +63 -0
- package/clis/chatgpt-app/ask.js +1 -1
- package/clis/chatgpt-app/ax.js +4 -2
- package/clis/chatgpt-app/ax.test.js +12 -0
- package/clis/chatgpt-app/model.js +1 -1
- package/clis/chatgpt-app/new.js +1 -1
- package/clis/chatgpt-app/read.js +1 -1
- package/clis/chatgpt-app/send.js +1 -1
- package/clis/chatgpt-app/status.js +1 -1
- package/clis/chatwise/ask.js +2 -2
- package/clis/chatwise/model.js +2 -2
- package/clis/chatwise/send.js +2 -2
- package/clis/claude/ask.js +128 -0
- package/clis/claude/ask.test.js +338 -0
- package/clis/claude/commands.test.js +118 -0
- package/clis/claude/detail.js +29 -0
- package/clis/claude/history.js +31 -0
- package/clis/claude/new.js +21 -0
- package/clis/claude/read.js +24 -0
- package/clis/claude/send.js +41 -0
- package/clis/claude/status.js +24 -0
- package/clis/claude/utils.js +440 -0
- package/clis/claude/utils.test.js +148 -0
- package/clis/codex/ask.js +2 -2
- package/clis/codex/send.js +2 -2
- package/clis/ctrip/search.js +1 -1
- package/clis/ctrip/search.test.js +4 -4
- package/clis/cursor/ask.js +2 -2
- package/clis/cursor/composer.js +2 -2
- package/clis/cursor/send.js +2 -2
- package/clis/deepseek/ask.js +17 -4
- package/clis/deepseek/ask.test.js +46 -0
- package/clis/deepseek/utils.js +55 -16
- package/clis/deepseek/utils.test.js +124 -5
- package/clis/doubao/utils.js +53 -11
- package/clis/doubao/utils.test.js +22 -2
- package/clis/eastmoney/announcement.js +1 -1
- package/clis/eastmoney/convertible.js +1 -1
- package/clis/eastmoney/etf.js +1 -1
- package/clis/eastmoney/holders.js +1 -1
- package/clis/eastmoney/index-board.js +1 -1
- package/clis/eastmoney/kline.js +1 -1
- package/clis/eastmoney/kuaixun.js +1 -1
- package/clis/eastmoney/longhu.js +1 -1
- package/clis/eastmoney/money-flow.js +1 -1
- package/clis/eastmoney/northbound.js +1 -1
- package/clis/eastmoney/quote.js +1 -1
- package/clis/eastmoney/rank.js +1 -1
- package/clis/eastmoney/sectors.js +1 -1
- package/clis/facebook/marketplace-inbox.js +83 -0
- package/clis/facebook/marketplace-listings.js +83 -0
- package/clis/facebook/marketplace.test.js +91 -0
- package/clis/google/news.js +1 -1
- package/clis/google/suggest.js +1 -1
- package/clis/google/trends.js +1 -1
- package/clis/google-scholar/cite.js +74 -0
- package/clis/google-scholar/cite.test.js +47 -0
- package/clis/google-scholar/profile.js +92 -0
- package/clis/google-scholar/profile.test.js +49 -0
- package/clis/google-scholar/search.js +1 -1
- package/clis/google-scholar/search.test.js +15 -0
- package/clis/hf/top.js +1 -1
- package/clis/instagram/collection-create.js +57 -0
- package/clis/instagram/saved.js +21 -7
- package/clis/jd/item.js +679 -47
- package/clis/jd/item.test.js +318 -7
- package/clis/jd/item.test.ts +517 -0
- package/clis/lesswrong/comments.js +1 -1
- package/clis/lesswrong/curated.js +1 -1
- package/clis/lesswrong/frontpage.js +1 -1
- package/clis/lesswrong/new.js +1 -1
- package/clis/lesswrong/read.js +1 -1
- package/clis/lesswrong/sequences.js +1 -1
- package/clis/lesswrong/shortform.js +1 -1
- package/clis/lesswrong/tag.js +1 -1
- package/clis/lesswrong/tags.js +1 -1
- package/clis/lesswrong/top-month.js +1 -1
- package/clis/lesswrong/top-week.js +1 -1
- package/clis/lesswrong/top-year.js +1 -1
- package/clis/lesswrong/top.js +1 -1
- package/clis/lesswrong/user-posts.js +1 -1
- package/clis/lesswrong/user.js +1 -1
- package/clis/paperreview/commands.test.js +6 -6
- package/clis/paperreview/feedback.js +1 -1
- package/clis/paperreview/review.js +1 -1
- package/clis/paperreview/submit.js +1 -1
- package/clis/producthunt/posts.js +1 -1
- package/clis/producthunt/today.js +1 -1
- package/clis/sinablog/search.js +1 -1
- package/clis/sinafinance/news.js +1 -1
- package/clis/sinafinance/stock.js +1 -1
- package/clis/sinafinance/stock.test.js +2 -2
- package/clis/spotify/spotify.js +6 -6
- package/clis/substack/search.js +1 -1
- package/clis/toutiao/articles.js +5 -6
- package/clis/toutiao/articles.test.js +22 -15
- package/clis/twitter/followers.js +2 -2
- package/clis/twitter/following.js +224 -73
- package/clis/twitter/following.test.js +277 -0
- package/clis/twitter/post.js +184 -47
- package/clis/twitter/post.test.js +114 -34
- package/clis/uiverse/_shared.js +63 -4
- package/clis/uiverse/_shared.test.js +7 -0
- package/clis/uiverse/code.js +1 -0
- package/clis/uiverse/navigation.test.js +12 -0
- package/clis/uiverse/preview.js +1 -0
- package/clis/web/read.js +319 -81
- package/clis/web/read.test.js +221 -5
- package/clis/weibo/favorites.js +169 -0
- package/clis/weibo/favorites.test.js +114 -0
- package/clis/weibo/publish.js +282 -0
- package/clis/weibo/publish.test.js +183 -0
- package/clis/weread/ranking.js +1 -1
- package/clis/weread/search-regression.test.js +8 -8
- package/clis/weread/search.js +1 -1
- package/clis/wikipedia/random.js +1 -1
- package/clis/wikipedia/search.js +1 -1
- package/clis/wikipedia/summary.js +1 -1
- package/clis/wikipedia/trending.js +1 -1
- package/clis/xianyu/chat.js +3 -3
- package/clis/xianyu/item.js +2 -2
- package/clis/xianyu/item.test.js +3 -3
- package/clis/xiaohongshu/search.js +17 -2
- package/clis/xiaohongshu/search.test.js +37 -1
- package/clis/xiaoyuzhou/download.js +1 -1
- package/clis/xiaoyuzhou/download.test.js +3 -3
- package/clis/xiaoyuzhou/episode.js +1 -1
- package/clis/xiaoyuzhou/podcast-episodes.js +1 -1
- package/clis/xiaoyuzhou/podcast-episodes.test.js +2 -2
- package/clis/xiaoyuzhou/podcast.js +1 -1
- package/clis/xiaoyuzhou/transcript.js +1 -1
- package/clis/xiaoyuzhou/transcript.test.js +5 -5
- package/clis/yollomi/models.js +1 -1
- package/clis/youtube/channel.js +24 -1
- package/clis/youtube/channel.test.js +59 -0
- package/clis/zhihu/answer.js +21 -162
- package/clis/zhihu/answer.test.js +26 -53
- package/clis/zhihu/collection.js +197 -0
- package/clis/zhihu/collection.test.js +290 -0
- package/clis/zhihu/collections.js +127 -0
- package/clis/zhihu/collections.test.js +182 -0
- package/clis/zhihu/comment.js +24 -305
- package/clis/zhihu/comment.test.js +31 -35
- package/clis/zhihu/favorite.js +44 -182
- package/clis/zhihu/favorite.test.js +30 -167
- package/clis/zhihu/follow.js +25 -56
- package/clis/zhihu/follow.test.js +20 -23
- package/clis/zhihu/like.js +22 -67
- package/clis/zhihu/like.test.js +19 -42
- package/clis/zhihu/search.js +3 -2
- package/clis/zhihu/write-shared.js +8 -1
- package/clis/zhihu/write-shared.test.js +1 -0
- package/clis/zlibrary/commands.test.js +75 -0
- package/clis/zlibrary/info.js +47 -0
- package/clis/zlibrary/search.js +46 -0
- package/clis/zlibrary/utils.js +136 -0
- package/dist/src/adapter-source.d.ts +11 -0
- package/dist/src/adapter-source.js +24 -0
- package/dist/src/adapter-source.test.js +29 -0
- package/dist/src/browser/base-page.d.ts +3 -1
- package/dist/src/browser/base-page.js +76 -1
- package/dist/src/browser/base-page.test.d.ts +1 -0
- package/dist/src/browser/base-page.test.js +74 -0
- package/dist/src/browser/bridge.d.ts +1 -2
- package/dist/src/browser/bridge.js +40 -41
- package/dist/src/browser/cdp.d.ts +1 -0
- package/dist/src/browser/cdp.js +3 -3
- package/dist/src/browser/daemon-client.d.ts +38 -4
- package/dist/src/browser/daemon-client.js +24 -7
- package/dist/src/browser/daemon-client.test.js +49 -0
- package/dist/src/browser/daemon-lifecycle.d.ts +23 -0
- package/dist/src/browser/daemon-lifecycle.js +67 -0
- package/dist/src/browser/daemon-version.d.ts +4 -0
- package/dist/src/browser/daemon-version.js +12 -0
- package/dist/src/browser/errors.js +3 -0
- package/dist/src/browser/errors.test.js +3 -0
- package/dist/src/browser/network-cache.d.ts +1 -0
- package/dist/src/browser/page.d.ts +3 -1
- package/dist/src/browser/page.js +10 -2
- package/dist/src/browser/profile.d.ts +14 -0
- package/dist/src/browser/profile.js +85 -0
- package/dist/src/build-manifest.d.ts +2 -0
- package/dist/src/build-manifest.js +13 -3
- package/dist/src/build-manifest.test.js +20 -2
- package/dist/src/cli.d.ts +6 -0
- package/dist/src/cli.js +477 -35
- package/dist/src/cli.test.js +303 -2
- package/dist/src/commanderAdapter.js +17 -9
- package/dist/src/commanderAdapter.test.js +67 -2
- package/dist/src/commands/daemon.d.ts +2 -0
- package/dist/src/commands/daemon.js +42 -1
- package/dist/src/commands/daemon.test.js +103 -2
- package/dist/src/completion-shared.js +1 -2
- package/dist/src/completion.test.js +3 -2
- package/dist/src/daemon.js +125 -41
- package/dist/src/doctor.d.ts +5 -6
- package/dist/src/doctor.js +77 -19
- package/dist/src/doctor.test.js +117 -0
- package/dist/src/engine.test.js +6 -5
- package/dist/src/errors.d.ts +14 -8
- package/dist/src/errors.js +36 -30
- package/dist/src/errors.test.js +5 -5
- package/dist/src/execution.d.ts +4 -0
- package/dist/src/execution.js +173 -25
- package/dist/src/execution.test.js +171 -1
- package/dist/src/main.js +10 -0
- package/dist/src/observation/artifact.d.ts +16 -0
- package/dist/src/observation/artifact.js +260 -0
- package/dist/src/observation/artifact.test.d.ts +1 -0
- package/dist/src/observation/artifact.test.js +121 -0
- package/dist/src/observation/events.d.ts +89 -0
- package/dist/src/observation/events.js +1 -0
- package/dist/src/observation/index.d.ts +7 -0
- package/dist/src/observation/index.js +7 -0
- package/dist/src/observation/manager.d.ts +9 -0
- package/dist/src/observation/manager.js +27 -0
- package/dist/src/observation/manager.test.d.ts +1 -0
- package/dist/src/observation/manager.test.js +13 -0
- package/dist/src/observation/redaction.d.ts +11 -0
- package/dist/src/observation/redaction.js +81 -0
- package/dist/src/observation/redaction.test.d.ts +1 -0
- package/dist/src/observation/redaction.test.js +32 -0
- package/dist/src/observation/retention.d.ts +32 -0
- package/dist/src/observation/retention.js +160 -0
- package/dist/src/observation/retention.test.d.ts +1 -0
- package/dist/src/observation/retention.test.js +118 -0
- package/dist/src/observation/ring-buffer.d.ts +22 -0
- package/dist/src/observation/ring-buffer.js +45 -0
- package/dist/src/observation/ring-buffer.test.d.ts +1 -0
- package/dist/src/observation/ring-buffer.test.js +22 -0
- package/dist/src/observation/session.d.ts +25 -0
- package/dist/src/observation/session.js +50 -0
- package/dist/src/pipeline/executor.test.js +1 -0
- package/dist/src/pipeline/steps/download.test.js +1 -0
- package/dist/src/pipeline/steps/fetch.js +1 -21
- package/dist/src/pipeline/steps/fetch.test.js +6 -12
- package/dist/src/plugin-scaffold.js +1 -1
- package/dist/src/plugin-scaffold.test.js +1 -1
- package/dist/src/registry.d.ts +40 -9
- package/dist/src/registry.js +3 -1
- package/dist/src/runtime-detect.d.ts +10 -0
- package/dist/src/runtime-detect.js +19 -0
- package/dist/src/runtime-detect.test.js +12 -1
- package/dist/src/runtime.d.ts +2 -0
- package/dist/src/runtime.js +1 -0
- package/dist/src/types.d.ts +22 -0
- package/dist/src/update-check.d.ts +31 -1
- package/dist/src/update-check.js +62 -16
- package/dist/src/update-check.test.js +86 -1
- package/package.json +1 -1
- package/dist/src/diagnostic.d.ts +0 -63
- package/dist/src/diagnostic.js +0 -292
- package/dist/src/diagnostic.test.js +0 -302
- /package/dist/src/{diagnostic.test.d.ts → adapter-source.test.d.ts} +0 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import type { BrowserSessionInfo } from '../types.js';
|
|
7
7
|
export interface DaemonCommand {
|
|
8
8
|
id: string;
|
|
9
|
-
action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | 'insert-text' | 'bind
|
|
9
|
+
action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | 'insert-text' | 'bind' | 'network-capture-start' | 'network-capture-read' | 'cdp' | 'frames';
|
|
10
10
|
/** Target page identity (targetId). Cross-layer contract with the extension. */
|
|
11
11
|
page?: string;
|
|
12
12
|
code?: string;
|
|
@@ -30,21 +30,32 @@ export interface DaemonCommand {
|
|
|
30
30
|
pattern?: string;
|
|
31
31
|
cdpMethod?: string;
|
|
32
32
|
cdpParams?: Record<string, unknown>;
|
|
33
|
-
/** When true, automation
|
|
33
|
+
/** When true, the owned automation container is created in the foreground */
|
|
34
34
|
windowFocused?: boolean;
|
|
35
35
|
/** Custom idle timeout in seconds for this workspace session. Overrides the default. */
|
|
36
36
|
idleTimeout?: number;
|
|
37
|
+
/** Explicitly allow navigation inside a borrowed bound tab. */
|
|
38
|
+
allowBoundNavigation?: boolean;
|
|
37
39
|
/** Frame index for cross-frame operations (0-based, from 'frames' action) */
|
|
38
40
|
frameIndex?: number;
|
|
41
|
+
/** Browser profile/context to route the command to. */
|
|
42
|
+
contextId?: string;
|
|
39
43
|
}
|
|
40
44
|
export interface DaemonResult {
|
|
41
45
|
id: string;
|
|
42
46
|
ok: boolean;
|
|
43
47
|
data?: unknown;
|
|
44
48
|
error?: string;
|
|
49
|
+
errorCode?: string;
|
|
50
|
+
errorHint?: string;
|
|
45
51
|
/** Page identity (targetId) — present on page-scoped command responses */
|
|
46
52
|
page?: string;
|
|
47
53
|
}
|
|
54
|
+
export declare class BrowserCommandError extends Error {
|
|
55
|
+
readonly code?: string | undefined;
|
|
56
|
+
readonly hint?: string | undefined;
|
|
57
|
+
constructor(message: string, code?: string | undefined, hint?: string | undefined);
|
|
58
|
+
}
|
|
48
59
|
export interface DaemonStatus {
|
|
49
60
|
ok: boolean;
|
|
50
61
|
pid: number;
|
|
@@ -53,12 +64,25 @@ export interface DaemonStatus {
|
|
|
53
64
|
extensionConnected: boolean;
|
|
54
65
|
extensionVersion?: string;
|
|
55
66
|
extensionCompatRange?: string;
|
|
67
|
+
contextId?: string;
|
|
68
|
+
profileRequired?: boolean;
|
|
69
|
+
profileDisconnected?: boolean;
|
|
70
|
+
profiles?: BrowserProfileStatus[];
|
|
56
71
|
pending: number;
|
|
57
72
|
memoryMB: number;
|
|
58
73
|
port: number;
|
|
59
74
|
}
|
|
75
|
+
export interface BrowserProfileStatus {
|
|
76
|
+
contextId: string;
|
|
77
|
+
extensionConnected: boolean;
|
|
78
|
+
extensionVersion?: string;
|
|
79
|
+
extensionCompatRange?: string;
|
|
80
|
+
pending: number;
|
|
81
|
+
lastSeenAt?: number;
|
|
82
|
+
}
|
|
60
83
|
export declare function fetchDaemonStatus(opts?: {
|
|
61
84
|
timeout?: number;
|
|
85
|
+
contextId?: string;
|
|
62
86
|
}): Promise<DaemonStatus | null>;
|
|
63
87
|
export type DaemonHealth = {
|
|
64
88
|
state: 'stopped';
|
|
@@ -66,6 +90,12 @@ export type DaemonHealth = {
|
|
|
66
90
|
} | {
|
|
67
91
|
state: 'no-extension';
|
|
68
92
|
status: DaemonStatus;
|
|
93
|
+
} | {
|
|
94
|
+
state: 'profile-required';
|
|
95
|
+
status: DaemonStatus;
|
|
96
|
+
} | {
|
|
97
|
+
state: 'profile-disconnected';
|
|
98
|
+
status: DaemonStatus;
|
|
69
99
|
} | {
|
|
70
100
|
state: 'ready';
|
|
71
101
|
status: DaemonStatus;
|
|
@@ -76,6 +106,7 @@ export type DaemonHealth = {
|
|
|
76
106
|
*/
|
|
77
107
|
export declare function getDaemonHealth(opts?: {
|
|
78
108
|
timeout?: number;
|
|
109
|
+
contextId?: string;
|
|
79
110
|
}): Promise<DaemonHealth>;
|
|
80
111
|
export declare function requestDaemonShutdown(opts?: {
|
|
81
112
|
timeout?: number;
|
|
@@ -92,8 +123,11 @@ export declare function sendCommandFull(action: DaemonCommand['action'], params?
|
|
|
92
123
|
data: unknown;
|
|
93
124
|
page?: string;
|
|
94
125
|
}>;
|
|
95
|
-
export declare function listSessions(
|
|
96
|
-
|
|
126
|
+
export declare function listSessions(opts?: {
|
|
127
|
+
contextId?: string;
|
|
128
|
+
}): Promise<BrowserSessionInfo[]>;
|
|
129
|
+
export declare function bindTab(workspace: string, opts?: {
|
|
97
130
|
matchDomain?: string;
|
|
98
131
|
matchPathPrefix?: string;
|
|
132
|
+
contextId?: string;
|
|
99
133
|
}): Promise<unknown>;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { DEFAULT_DAEMON_PORT } from '../constants.js';
|
|
7
7
|
import { sleep } from '../utils.js';
|
|
8
8
|
import { classifyBrowserError } from './errors.js';
|
|
9
|
+
import { resolveProfileContextId } from './profile.js';
|
|
9
10
|
const DAEMON_PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
|
|
10
11
|
const DAEMON_URL = `http://127.0.0.1:${DAEMON_PORT}`;
|
|
11
12
|
const OPENCLI_HEADERS = { 'X-OpenCLI': '1' };
|
|
@@ -13,6 +14,16 @@ let _idCounter = 0;
|
|
|
13
14
|
function generateId() {
|
|
14
15
|
return `cmd_${process.pid}_${Date.now()}_${++_idCounter}`;
|
|
15
16
|
}
|
|
17
|
+
export class BrowserCommandError extends Error {
|
|
18
|
+
code;
|
|
19
|
+
hint;
|
|
20
|
+
constructor(message, code, hint) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.hint = hint;
|
|
24
|
+
this.name = 'BrowserCommandError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
16
27
|
async function requestDaemon(pathname, init) {
|
|
17
28
|
const { timeout = 2000, headers, ...rest } = init ?? {};
|
|
18
29
|
const controller = new AbortController();
|
|
@@ -30,7 +41,8 @@ async function requestDaemon(pathname, init) {
|
|
|
30
41
|
}
|
|
31
42
|
export async function fetchDaemonStatus(opts) {
|
|
32
43
|
try {
|
|
33
|
-
const
|
|
44
|
+
const params = opts?.contextId ? `?contextId=${encodeURIComponent(opts.contextId)}` : '';
|
|
45
|
+
const res = await requestDaemon(`/status${params}`, { timeout: opts?.timeout ?? 2000 });
|
|
34
46
|
if (!res.ok)
|
|
35
47
|
return null;
|
|
36
48
|
return await res.json();
|
|
@@ -47,6 +59,10 @@ export async function getDaemonHealth(opts) {
|
|
|
47
59
|
const status = await fetchDaemonStatus(opts);
|
|
48
60
|
if (!status)
|
|
49
61
|
return { state: 'stopped', status: null };
|
|
62
|
+
if (status.profileRequired)
|
|
63
|
+
return { state: 'profile-required', status };
|
|
64
|
+
if (status.profileDisconnected)
|
|
65
|
+
return { state: 'profile-disconnected', status };
|
|
50
66
|
if (!status.extensionConnected)
|
|
51
67
|
return { state: 'no-extension', status };
|
|
52
68
|
return { state: 'ready', status };
|
|
@@ -75,7 +91,8 @@ async function sendCommandRaw(action, params) {
|
|
|
75
91
|
const id = generateId();
|
|
76
92
|
const wf = process.env.OPENCLI_WINDOW_FOCUSED;
|
|
77
93
|
const windowFocused = (wf === '1' || wf === 'true') ? true : undefined;
|
|
78
|
-
const
|
|
94
|
+
const contextId = params.contextId ?? resolveProfileContextId();
|
|
95
|
+
const command = { id, action, ...params, ...(contextId && { contextId }), ...(windowFocused && { windowFocused }) };
|
|
79
96
|
try {
|
|
80
97
|
const res = await requestDaemon('/command', {
|
|
81
98
|
method: 'POST',
|
|
@@ -95,7 +112,7 @@ async function sendCommandRaw(action, params) {
|
|
|
95
112
|
await sleep(advice.delayMs);
|
|
96
113
|
continue;
|
|
97
114
|
}
|
|
98
|
-
throw new
|
|
115
|
+
throw new BrowserCommandError(result.error ?? 'Daemon command failed', result.errorCode, result.errorHint);
|
|
99
116
|
}
|
|
100
117
|
return result;
|
|
101
118
|
}
|
|
@@ -126,10 +143,10 @@ export async function sendCommandFull(action, params = {}) {
|
|
|
126
143
|
const result = await sendCommandRaw(action, params);
|
|
127
144
|
return { data: result.data, page: result.page };
|
|
128
145
|
}
|
|
129
|
-
export async function listSessions() {
|
|
130
|
-
const result = await sendCommand('sessions');
|
|
146
|
+
export async function listSessions(opts) {
|
|
147
|
+
const result = await sendCommand('sessions', { ...(opts?.contextId && { contextId: opts.contextId }) });
|
|
131
148
|
return Array.isArray(result) ? result : [];
|
|
132
149
|
}
|
|
133
|
-
export async function
|
|
134
|
-
return sendCommand('bind
|
|
150
|
+
export async function bindTab(workspace, opts = {}) {
|
|
151
|
+
return sendCommand('bind', { workspace, ...opts });
|
|
135
152
|
}
|
|
@@ -6,6 +6,7 @@ describe('daemon-client', () => {
|
|
|
6
6
|
});
|
|
7
7
|
afterEach(() => {
|
|
8
8
|
vi.restoreAllMocks();
|
|
9
|
+
vi.unstubAllEnvs();
|
|
9
10
|
});
|
|
10
11
|
it('fetchDaemonStatus sends the shared status request and returns parsed data', async () => {
|
|
11
12
|
const status = {
|
|
@@ -78,6 +79,43 @@ describe('daemon-client', () => {
|
|
|
78
79
|
});
|
|
79
80
|
await expect(getDaemonHealth()).resolves.toEqual({ state: 'ready', status });
|
|
80
81
|
});
|
|
82
|
+
it('getDaemonHealth returns profile-required when multiple profiles are connected without a selection', async () => {
|
|
83
|
+
const status = {
|
|
84
|
+
ok: true,
|
|
85
|
+
pid: 123,
|
|
86
|
+
uptime: 10,
|
|
87
|
+
extensionConnected: false,
|
|
88
|
+
profileRequired: true,
|
|
89
|
+
profiles: [
|
|
90
|
+
{ contextId: 'work', extensionConnected: true, pending: 0 },
|
|
91
|
+
{ contextId: 'personal', extensionConnected: true, pending: 0 },
|
|
92
|
+
],
|
|
93
|
+
pending: 0,
|
|
94
|
+
memoryMB: 32,
|
|
95
|
+
port: 19825,
|
|
96
|
+
};
|
|
97
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
98
|
+
ok: true,
|
|
99
|
+
json: () => Promise.resolve(status),
|
|
100
|
+
});
|
|
101
|
+
await expect(getDaemonHealth()).resolves.toEqual({ state: 'profile-required', status });
|
|
102
|
+
});
|
|
103
|
+
it('fetchDaemonStatus includes contextId in the status query', async () => {
|
|
104
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
105
|
+
ok: true,
|
|
106
|
+
json: () => Promise.resolve({
|
|
107
|
+
ok: true,
|
|
108
|
+
pid: 1,
|
|
109
|
+
uptime: 0,
|
|
110
|
+
extensionConnected: true,
|
|
111
|
+
pending: 0,
|
|
112
|
+
memoryMB: 1,
|
|
113
|
+
port: 19825,
|
|
114
|
+
}),
|
|
115
|
+
});
|
|
116
|
+
await fetchDaemonStatus({ contextId: 'work' });
|
|
117
|
+
expect(vi.mocked(fetch).mock.calls[0][0]).toMatch(/\/status\?contextId=work$/);
|
|
118
|
+
});
|
|
81
119
|
it('sendCommand includes the current pid in generated command ids', async () => {
|
|
82
120
|
vi.spyOn(Date, 'now').mockReturnValue(1_763_000_000_000);
|
|
83
121
|
vi.mocked(fetch).mockResolvedValue({
|
|
@@ -95,6 +133,17 @@ describe('daemon-client', () => {
|
|
|
95
133
|
expect(ids[1]).toMatch(new RegExp(`^cmd_${process.pid}_1763000000000_\\d+$`));
|
|
96
134
|
expect(ids[0]).not.toBe(ids[1]);
|
|
97
135
|
});
|
|
136
|
+
it('sendCommand forwards OPENCLI_PROFILE as command contextId', async () => {
|
|
137
|
+
vi.stubEnv('OPENCLI_PROFILE', 'work');
|
|
138
|
+
vi.spyOn(Date, 'now').mockReturnValue(1_763_000_000_000);
|
|
139
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
140
|
+
status: 200,
|
|
141
|
+
json: () => Promise.resolve({ id: 'server', ok: true, data: 'ok' }),
|
|
142
|
+
});
|
|
143
|
+
await sendCommand('exec', { code: '1 + 1' });
|
|
144
|
+
const body = JSON.parse(String(vi.mocked(fetch).mock.calls[0][1]?.body));
|
|
145
|
+
expect(body.contextId).toBe('work');
|
|
146
|
+
});
|
|
98
147
|
it('sendCommand retries with a new id when daemon reports a duplicate pending id', async () => {
|
|
99
148
|
vi.spyOn(Date, 'now').mockReturnValue(1_763_000_000_123);
|
|
100
149
|
const fetchMock = vi.mocked(fetch);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ChildProcess } from 'node:child_process';
|
|
2
|
+
import { DEFAULT_DAEMON_PORT } from '../constants.js';
|
|
3
|
+
import { type DaemonStatus } from './daemon-client.js';
|
|
4
|
+
export interface DaemonLaunchSpec {
|
|
5
|
+
binary: string;
|
|
6
|
+
args: string[];
|
|
7
|
+
scriptPath: string;
|
|
8
|
+
}
|
|
9
|
+
export interface DaemonRestartResult {
|
|
10
|
+
previousStatus: DaemonStatus | null;
|
|
11
|
+
status: DaemonStatus | null;
|
|
12
|
+
stopped: boolean;
|
|
13
|
+
spawned: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function resolveDaemonLaunchSpec(): DaemonLaunchSpec;
|
|
16
|
+
export declare function spawnDaemonProcess(): ChildProcess;
|
|
17
|
+
export declare function waitForDaemonStop(timeoutMs: number): Promise<boolean>;
|
|
18
|
+
export declare function waitForDaemonStatus(timeoutMs: number): Promise<DaemonStatus | null>;
|
|
19
|
+
export declare function restartDaemon(opts?: {
|
|
20
|
+
stopTimeoutMs?: number;
|
|
21
|
+
startTimeoutMs?: number;
|
|
22
|
+
}): Promise<DaemonRestartResult>;
|
|
23
|
+
export { DEFAULT_DAEMON_PORT };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { DEFAULT_DAEMON_PORT } from '../constants.js';
|
|
6
|
+
import { fetchDaemonStatus, getDaemonHealth, requestDaemonShutdown } from './daemon-client.js';
|
|
7
|
+
export function resolveDaemonLaunchSpec() {
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const parentDir = path.resolve(__dirname, '..');
|
|
10
|
+
const daemonTs = path.join(parentDir, 'daemon.ts');
|
|
11
|
+
const daemonJs = path.join(parentDir, 'daemon.js');
|
|
12
|
+
const isTs = fs.existsSync(daemonTs);
|
|
13
|
+
const scriptPath = isTs ? daemonTs : daemonJs;
|
|
14
|
+
return {
|
|
15
|
+
binary: process.execPath,
|
|
16
|
+
args: isTs ? ['--import', 'tsx/esm', scriptPath] : [scriptPath],
|
|
17
|
+
scriptPath,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function spawnDaemonProcess() {
|
|
21
|
+
const launch = resolveDaemonLaunchSpec();
|
|
22
|
+
const proc = spawn(launch.binary, launch.args, {
|
|
23
|
+
detached: true,
|
|
24
|
+
stdio: 'ignore',
|
|
25
|
+
env: { ...process.env },
|
|
26
|
+
});
|
|
27
|
+
proc.unref();
|
|
28
|
+
return proc;
|
|
29
|
+
}
|
|
30
|
+
export async function waitForDaemonStop(timeoutMs) {
|
|
31
|
+
const deadline = Date.now() + timeoutMs;
|
|
32
|
+
while (Date.now() < deadline) {
|
|
33
|
+
await sleep(200);
|
|
34
|
+
const h = await getDaemonHealth();
|
|
35
|
+
if (h.state === 'stopped')
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
export async function waitForDaemonStatus(timeoutMs) {
|
|
41
|
+
const deadline = Date.now() + timeoutMs;
|
|
42
|
+
while (Date.now() < deadline) {
|
|
43
|
+
const status = await fetchDaemonStatus({ timeout: Math.min(1000, Math.max(100, deadline - Date.now())) });
|
|
44
|
+
if (status)
|
|
45
|
+
return status;
|
|
46
|
+
await sleep(200);
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
export async function restartDaemon(opts = {}) {
|
|
51
|
+
const previousStatus = await fetchDaemonStatus();
|
|
52
|
+
let stopped = previousStatus === null;
|
|
53
|
+
if (previousStatus) {
|
|
54
|
+
const shutdownAccepted = await requestDaemonShutdown();
|
|
55
|
+
stopped = shutdownAccepted && await waitForDaemonStop(opts.stopTimeoutMs ?? 3000);
|
|
56
|
+
if (!stopped) {
|
|
57
|
+
return { previousStatus, status: previousStatus, stopped: false, spawned: false };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
spawnDaemonProcess();
|
|
61
|
+
const status = await waitForDaemonStatus(opts.startTimeoutMs ?? 5000);
|
|
62
|
+
return { previousStatus, status, stopped, spawned: true };
|
|
63
|
+
}
|
|
64
|
+
function sleep(ms) {
|
|
65
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
66
|
+
}
|
|
67
|
+
export { DEFAULT_DAEMON_PORT };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DaemonStatus } from './daemon-client.js';
|
|
2
|
+
export declare function isDaemonStale(status: Pick<DaemonStatus, 'daemonVersion'> | null | undefined, cliVersion?: string): boolean;
|
|
3
|
+
export declare function formatDaemonVersion(status: Pick<DaemonStatus, 'daemonVersion'> | null | undefined): string;
|
|
4
|
+
export declare function staleDaemonIssue(status: Pick<DaemonStatus, 'daemonVersion'> | null | undefined, cliVersion: string): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function isDaemonStale(status, cliVersion) {
|
|
2
|
+
if (!status || !cliVersion)
|
|
3
|
+
return false;
|
|
4
|
+
return !status.daemonVersion || status.daemonVersion !== cliVersion;
|
|
5
|
+
}
|
|
6
|
+
export function formatDaemonVersion(status) {
|
|
7
|
+
return status?.daemonVersion ? `v${status.daemonVersion}` : 'version unknown';
|
|
8
|
+
}
|
|
9
|
+
export function staleDaemonIssue(status, cliVersion) {
|
|
10
|
+
return `Stale daemon detected: daemon ${formatDaemonVersion(status)} != CLI v${cliVersion}.\n` +
|
|
11
|
+
' Run: opencli daemon restart';
|
|
12
|
+
}
|
|
@@ -15,7 +15,10 @@ const EXTENSION_TRANSIENT_PATTERNS = [
|
|
|
15
15
|
'Extension disconnected',
|
|
16
16
|
'Extension not connected',
|
|
17
17
|
'attach failed',
|
|
18
|
+
'Detached while handling command',
|
|
19
|
+
'Debugger is not attached to the tab',
|
|
18
20
|
'no longer exists',
|
|
21
|
+
'No tab with id',
|
|
19
22
|
'CDP connection',
|
|
20
23
|
'Daemon command failed',
|
|
21
24
|
'No window with id',
|
|
@@ -6,7 +6,10 @@ describe('classifyBrowserError', () => {
|
|
|
6
6
|
'Extension disconnected',
|
|
7
7
|
'Extension not connected',
|
|
8
8
|
'attach failed',
|
|
9
|
+
'Detached while handling command',
|
|
10
|
+
'Debugger is not attached to the tab: 123',
|
|
9
11
|
'no longer exists',
|
|
12
|
+
'No tab with id: 456',
|
|
10
13
|
'CDP connection reset',
|
|
11
14
|
'Daemon command failed',
|
|
12
15
|
'No window with id: 123',
|
|
@@ -15,8 +15,9 @@ import { BasePage } from './base-page.js';
|
|
|
15
15
|
*/
|
|
16
16
|
export declare class Page extends BasePage {
|
|
17
17
|
private readonly workspace;
|
|
18
|
+
readonly contextId?: string | undefined;
|
|
18
19
|
private readonly _idleTimeout;
|
|
19
|
-
constructor(workspace?: string, idleTimeout?: number);
|
|
20
|
+
constructor(workspace?: string, idleTimeout?: number, contextId?: string | undefined);
|
|
20
21
|
/** Active page identity (targetId), set after navigate and used in all subsequent commands */
|
|
21
22
|
private _page;
|
|
22
23
|
private _networkCaptureUnsupported;
|
|
@@ -28,6 +29,7 @@ export declare class Page extends BasePage {
|
|
|
28
29
|
goto(url: string, options?: {
|
|
29
30
|
waitUntil?: 'load' | 'none';
|
|
30
31
|
settleMs?: number;
|
|
32
|
+
allowBoundNavigation?: boolean;
|
|
31
33
|
}): Promise<void>;
|
|
32
34
|
/** Get the active page identity (targetId) */
|
|
33
35
|
getActivePage(): string | undefined;
|
package/dist/src/browser/page.js
CHANGED
|
@@ -27,10 +27,12 @@ function isUnsupportedNetworkCaptureError(err) {
|
|
|
27
27
|
*/
|
|
28
28
|
export class Page extends BasePage {
|
|
29
29
|
workspace;
|
|
30
|
+
contextId;
|
|
30
31
|
_idleTimeout;
|
|
31
|
-
constructor(workspace = 'default', idleTimeout) {
|
|
32
|
+
constructor(workspace = 'default', idleTimeout, contextId) {
|
|
32
33
|
super();
|
|
33
34
|
this.workspace = workspace;
|
|
35
|
+
this.contextId = contextId;
|
|
34
36
|
this._idleTimeout = idleTimeout;
|
|
35
37
|
}
|
|
36
38
|
/** Active page identity (targetId), set after navigate and used in all subsequent commands */
|
|
@@ -39,12 +41,17 @@ export class Page extends BasePage {
|
|
|
39
41
|
_networkCaptureWarned = false;
|
|
40
42
|
/** Helper: spread workspace into command params */
|
|
41
43
|
_wsOpt() {
|
|
42
|
-
return {
|
|
44
|
+
return {
|
|
45
|
+
workspace: this.workspace,
|
|
46
|
+
...(this.contextId && { contextId: this.contextId }),
|
|
47
|
+
...(this._idleTimeout != null && { idleTimeout: this._idleTimeout }),
|
|
48
|
+
};
|
|
43
49
|
}
|
|
44
50
|
/** Helper: spread workspace + page identity into command params */
|
|
45
51
|
_cmdOpts() {
|
|
46
52
|
return {
|
|
47
53
|
workspace: this.workspace,
|
|
54
|
+
...(this.contextId && { contextId: this.contextId }),
|
|
48
55
|
...(this._page !== undefined && { page: this._page }),
|
|
49
56
|
...(this._idleTimeout != null && { idleTimeout: this._idleTimeout }),
|
|
50
57
|
};
|
|
@@ -53,6 +60,7 @@ export class Page extends BasePage {
|
|
|
53
60
|
const result = await sendCommandFull('navigate', {
|
|
54
61
|
url,
|
|
55
62
|
...this._cmdOpts(),
|
|
63
|
+
...(options?.allowBoundNavigation === true && { allowBoundNavigation: true }),
|
|
56
64
|
});
|
|
57
65
|
// Remember the page identity (targetId) for subsequent calls
|
|
58
66
|
if (result.page) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const DEFAULT_CONTEXT_ID = "default";
|
|
2
|
+
export type ProfileConfig = {
|
|
3
|
+
version: 1;
|
|
4
|
+
defaultContextId?: string;
|
|
5
|
+
aliases: Record<string, string>;
|
|
6
|
+
};
|
|
7
|
+
export declare function normalizeContextId(value: string | undefined | null): string | undefined;
|
|
8
|
+
export declare function emptyProfileConfig(): ProfileConfig;
|
|
9
|
+
export declare function loadProfileConfig(): ProfileConfig;
|
|
10
|
+
export declare function saveProfileConfig(config: ProfileConfig): void;
|
|
11
|
+
export declare function resolveProfileContextId(profile?: string): string | undefined;
|
|
12
|
+
export declare function aliasForContextId(config: ProfileConfig, contextId: string): string | undefined;
|
|
13
|
+
export declare function renameProfile(contextId: string, alias: string): ProfileConfig;
|
|
14
|
+
export declare function setDefaultProfile(profile: string): ProfileConfig;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
export const DEFAULT_CONTEXT_ID = 'default';
|
|
5
|
+
function profileConfigPath() {
|
|
6
|
+
const baseDir = process.env.OPENCLI_CONFIG_DIR || path.join(os.homedir(), '.opencli');
|
|
7
|
+
return path.join(baseDir, 'browser-profiles.json');
|
|
8
|
+
}
|
|
9
|
+
export function normalizeContextId(value) {
|
|
10
|
+
const trimmed = value?.trim();
|
|
11
|
+
return trimmed || undefined;
|
|
12
|
+
}
|
|
13
|
+
export function emptyProfileConfig() {
|
|
14
|
+
return { version: 1, aliases: {} };
|
|
15
|
+
}
|
|
16
|
+
export function loadProfileConfig() {
|
|
17
|
+
try {
|
|
18
|
+
const raw = fs.readFileSync(profileConfigPath(), 'utf-8');
|
|
19
|
+
const parsed = JSON.parse(raw);
|
|
20
|
+
const aliases = parsed.aliases && typeof parsed.aliases === 'object'
|
|
21
|
+
? Object.fromEntries(Object.entries(parsed.aliases).filter((entry) => {
|
|
22
|
+
const [key, value] = entry;
|
|
23
|
+
return typeof key === 'string' && key.trim().length > 0
|
|
24
|
+
&& typeof value === 'string' && value.trim().length > 0;
|
|
25
|
+
}))
|
|
26
|
+
: {};
|
|
27
|
+
return {
|
|
28
|
+
version: 1,
|
|
29
|
+
aliases,
|
|
30
|
+
...(typeof parsed.defaultContextId === 'string' && parsed.defaultContextId.trim()
|
|
31
|
+
? { defaultContextId: parsed.defaultContextId.trim() }
|
|
32
|
+
: {}),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return emptyProfileConfig();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function saveProfileConfig(config) {
|
|
40
|
+
const target = profileConfigPath();
|
|
41
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
42
|
+
fs.writeFileSync(target, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
43
|
+
}
|
|
44
|
+
export function resolveProfileContextId(profile) {
|
|
45
|
+
const config = loadProfileConfig();
|
|
46
|
+
const requested = normalizeContextId(profile)
|
|
47
|
+
?? normalizeContextId(process.env.OPENCLI_PROFILE)
|
|
48
|
+
?? normalizeContextId(config.defaultContextId);
|
|
49
|
+
if (!requested)
|
|
50
|
+
return undefined;
|
|
51
|
+
return config.aliases[requested] ?? requested;
|
|
52
|
+
}
|
|
53
|
+
export function aliasForContextId(config, contextId) {
|
|
54
|
+
for (const [alias, id] of Object.entries(config.aliases)) {
|
|
55
|
+
if (id === contextId)
|
|
56
|
+
return alias;
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
export function renameProfile(contextId, alias) {
|
|
61
|
+
const normalizedContextId = normalizeContextId(contextId);
|
|
62
|
+
const normalizedAlias = normalizeContextId(alias);
|
|
63
|
+
if (!normalizedContextId)
|
|
64
|
+
throw new Error('profile contextId is required');
|
|
65
|
+
if (!normalizedAlias)
|
|
66
|
+
throw new Error('profile alias is required');
|
|
67
|
+
const config = loadProfileConfig();
|
|
68
|
+
for (const [existingAlias, existingContextId] of Object.entries(config.aliases)) {
|
|
69
|
+
if (existingAlias !== normalizedAlias && existingContextId === normalizedContextId) {
|
|
70
|
+
delete config.aliases[existingAlias];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
config.aliases[normalizedAlias] = normalizedContextId;
|
|
74
|
+
saveProfileConfig(config);
|
|
75
|
+
return config;
|
|
76
|
+
}
|
|
77
|
+
export function setDefaultProfile(profile) {
|
|
78
|
+
const contextId = resolveProfileContextId(profile) ?? normalizeContextId(profile);
|
|
79
|
+
if (!contextId)
|
|
80
|
+
throw new Error('profile is required');
|
|
81
|
+
const config = loadProfileConfig();
|
|
82
|
+
config.defaultContextId = contextId;
|
|
83
|
+
saveProfileConfig(config);
|
|
84
|
+
return config;
|
|
85
|
+
}
|
|
@@ -39,5 +39,7 @@ export interface ManifestEntry {
|
|
|
39
39
|
/** Pre-navigation control — see CliCommand.navigateBefore */
|
|
40
40
|
navigateBefore?: boolean | string;
|
|
41
41
|
}
|
|
42
|
+
export declare function normalizeManifestPath(relativePath: string): string;
|
|
42
43
|
export declare function loadManifestEntries(filePath: string, site: string, importer?: (moduleHref: string) => Promise<unknown>): Promise<ManifestEntry[]>;
|
|
43
44
|
export declare function buildManifest(): Promise<ManifestEntry[]>;
|
|
45
|
+
export declare function serializeManifest(manifest: ManifestEntry[]): string;
|
|
@@ -36,6 +36,12 @@ function toModulePath(filePath, site) {
|
|
|
36
36
|
const baseName = path.basename(filePath, path.extname(filePath));
|
|
37
37
|
return `${site}/${baseName}.js`;
|
|
38
38
|
}
|
|
39
|
+
export function normalizeManifestPath(relativePath) {
|
|
40
|
+
return relativePath.replace(/\\/g, '/');
|
|
41
|
+
}
|
|
42
|
+
function toManifestRelativePath(filePath) {
|
|
43
|
+
return normalizeManifestPath(path.relative(CLIS_DIR, filePath));
|
|
44
|
+
}
|
|
39
45
|
function isCliCommandValue(value, site) {
|
|
40
46
|
return isRecord(value)
|
|
41
47
|
&& typeof value.site === 'string'
|
|
@@ -85,8 +91,9 @@ export async function loadManifestEntries(filePath, site, importer = moduleHref
|
|
|
85
91
|
return !previous || previous !== cmd;
|
|
86
92
|
})
|
|
87
93
|
.map(([, cmd]) => cmd);
|
|
88
|
-
//
|
|
89
|
-
|
|
94
|
+
// Manifest paths are cross-platform artifacts; keep them POSIX-style even
|
|
95
|
+
// when build-manifest runs on Windows.
|
|
96
|
+
const sourceRelative = toManifestRelativePath(filePath);
|
|
90
97
|
const seen = new Set();
|
|
91
98
|
return runtimeCommands
|
|
92
99
|
.filter((cmd) => {
|
|
@@ -128,10 +135,13 @@ export async function buildManifest() {
|
|
|
128
135
|
}
|
|
129
136
|
return [...manifest.values()].sort((a, b) => a.site.localeCompare(b.site) || a.name.localeCompare(b.name));
|
|
130
137
|
}
|
|
138
|
+
export function serializeManifest(manifest) {
|
|
139
|
+
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
140
|
+
}
|
|
131
141
|
async function main() {
|
|
132
142
|
const manifest = await buildManifest();
|
|
133
143
|
fs.mkdirSync(path.dirname(OUTPUT), { recursive: true });
|
|
134
|
-
fs.writeFileSync(OUTPUT,
|
|
144
|
+
fs.writeFileSync(OUTPUT, serializeManifest(manifest));
|
|
135
145
|
console.log(`✅ Manifest compiled: ${manifest.length} entries → ${OUTPUT}`);
|
|
136
146
|
// Restore executable permissions on bin entries.
|
|
137
147
|
// tsc does not preserve the +x bit, so after a clean rebuild the CLI
|
|
@@ -3,7 +3,7 @@ import * as fs from 'node:fs';
|
|
|
3
3
|
import * as os from 'node:os';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
5
|
import { cli, getRegistry, Strategy } from './registry.js';
|
|
6
|
-
import { loadManifestEntries } from './build-manifest.js';
|
|
6
|
+
import { loadManifestEntries, normalizeManifestPath, serializeManifest } from './build-manifest.js';
|
|
7
7
|
describe('manifest helper rules', () => {
|
|
8
8
|
const tempDirs = [];
|
|
9
9
|
afterEach(() => {
|
|
@@ -76,8 +76,9 @@ describe('manifest helper rules', () => {
|
|
|
76
76
|
replacedBy: 'opencli demo new',
|
|
77
77
|
},
|
|
78
78
|
]);
|
|
79
|
-
// Verify sourceFile is included
|
|
79
|
+
// Verify sourceFile is included and stable for manifest consumers.
|
|
80
80
|
expect(entries[0].sourceFile).toBeDefined();
|
|
81
|
+
expect(entries[0].sourceFile).not.toContain('\\');
|
|
81
82
|
getRegistry().delete(key);
|
|
82
83
|
});
|
|
83
84
|
it('falls back to registry delta for side-effect-only cli modules', async () => {
|
|
@@ -139,4 +140,21 @@ describe('manifest helper rules', () => {
|
|
|
139
140
|
getRegistry().delete(screenKey);
|
|
140
141
|
getRegistry().delete(statusKey);
|
|
141
142
|
});
|
|
143
|
+
it('normalizes manifest paths to POSIX separators', () => {
|
|
144
|
+
expect(normalizeManifestPath('demo\\status.js')).toBe('demo/status.js');
|
|
145
|
+
expect(normalizeManifestPath('demo/status.js')).toBe('demo/status.js');
|
|
146
|
+
});
|
|
147
|
+
it('serializes manifest json with a trailing newline', () => {
|
|
148
|
+
const serialized = serializeManifest([{
|
|
149
|
+
site: 'demo',
|
|
150
|
+
name: 'status',
|
|
151
|
+
description: '',
|
|
152
|
+
strategy: 'public',
|
|
153
|
+
browser: false,
|
|
154
|
+
args: [],
|
|
155
|
+
type: 'js',
|
|
156
|
+
}]);
|
|
157
|
+
expect(serialized.endsWith('\n')).toBe(true);
|
|
158
|
+
expect(serialized).toContain('\n]');
|
|
159
|
+
});
|
|
142
160
|
});
|
package/dist/src/cli.d.ts
CHANGED
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { Command } from 'commander';
|
|
8
8
|
import { findPackageRoot } from './package-paths.js';
|
|
9
|
+
export declare function selectFreshByTimestamp<T extends {
|
|
10
|
+
timestamp?: unknown;
|
|
11
|
+
}>(items: T[], lastSeenTs: number): {
|
|
12
|
+
fresh: T[];
|
|
13
|
+
lastSeenTs: number;
|
|
14
|
+
};
|
|
9
15
|
/**
|
|
10
16
|
* Check whether the site-memory scaffolding exists under
|
|
11
17
|
* ~/.opencli/sites/<site>/. Agents have a strong tendency to forget to write
|