@jackwener/opencli 1.5.3 → 1.5.5
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/.agents/skills/cross-project-adapter-migration/SKILL.md +3 -3
- package/README.md +213 -18
- package/dist/build-manifest.d.ts +2 -3
- package/dist/build-manifest.js +75 -170
- package/dist/build-manifest.test.js +113 -88
- package/dist/cli-manifest.json +1253 -1105
- package/dist/cli.js +14 -14
- package/dist/clis/antigravity/serve.js +2 -2
- package/dist/clis/sinafinance/rolling-news.d.ts +4 -0
- package/dist/clis/sinafinance/rolling-news.js +40 -0
- package/dist/clis/sinafinance/stock.d.ts +8 -0
- package/dist/clis/sinafinance/stock.js +117 -0
- package/dist/commanderAdapter.js +26 -3
- package/dist/daemon.js +19 -7
- package/dist/errors.d.ts +29 -1
- package/dist/errors.js +49 -11
- package/dist/external-clis.yaml +16 -0
- package/dist/external.js +3 -3
- package/dist/main.js +2 -1
- package/dist/serialization.js +6 -1
- package/dist/serialization.test.d.ts +1 -0
- package/dist/serialization.test.js +23 -0
- package/dist/tui.js +2 -1
- package/docs/adapters/browser/sinafinance.md +56 -6
- package/extension/dist/background.js +12 -6
- package/extension/manifest.json +2 -2
- package/extension/package-lock.json +2 -2
- package/extension/package.json +1 -1
- package/extension/src/background.ts +21 -6
- package/extension/src/protocol.ts +2 -1
- package/package.json +1 -1
- package/src/build-manifest.test.ts +117 -88
- package/src/build-manifest.ts +81 -180
- package/src/cli.ts +14 -14
- package/src/clis/antigravity/serve.ts +2 -2
- package/src/clis/sinafinance/rolling-news.ts +42 -0
- package/src/clis/sinafinance/stock.ts +127 -0
- package/src/commanderAdapter.ts +25 -2
- package/src/daemon.ts +21 -8
- package/src/errors.ts +71 -10
- package/src/external-clis.yaml +16 -0
- package/src/external.ts +3 -3
- package/src/main.ts +2 -1
- package/src/serialization.test.ts +26 -0
- package/src/serialization.ts +6 -1
- package/src/tui.ts +2 -1
package/dist/serialization.js
CHANGED
|
@@ -44,6 +44,11 @@ export function formatArgSummary(args) {
|
|
|
44
44
|
})
|
|
45
45
|
.join(' ');
|
|
46
46
|
}
|
|
47
|
+
function summarizeChoices(choices) {
|
|
48
|
+
if (choices.length <= 4)
|
|
49
|
+
return choices.join(', ');
|
|
50
|
+
return `${choices.slice(0, 4).join(', ')}, ... (+${choices.length - 4} more)`;
|
|
51
|
+
}
|
|
47
52
|
/** Generate the --help appendix showing registry metadata not exposed by Commander. */
|
|
48
53
|
export function formatRegistryHelpText(cmd) {
|
|
49
54
|
const lines = [];
|
|
@@ -51,7 +56,7 @@ export function formatRegistryHelpText(cmd) {
|
|
|
51
56
|
for (const a of choicesArgs) {
|
|
52
57
|
const prefix = a.positional ? `<${a.name}>` : `--${a.name}`;
|
|
53
58
|
const def = a.default != null ? ` (default: ${a.default})` : '';
|
|
54
|
-
lines.push(` ${prefix}: ${a.choices
|
|
59
|
+
lines.push(` ${prefix}: ${summarizeChoices(a.choices)}${def}`);
|
|
55
60
|
}
|
|
56
61
|
const meta = [];
|
|
57
62
|
meta.push(`Strategy: ${strategyLabel(cmd)}`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Strategy } from './registry.js';
|
|
3
|
+
import { formatRegistryHelpText } from './serialization.js';
|
|
4
|
+
describe('formatRegistryHelpText', () => {
|
|
5
|
+
it('summarizes long choices lists so help text stays readable', () => {
|
|
6
|
+
const cmd = {
|
|
7
|
+
site: 'demo',
|
|
8
|
+
name: 'dynamic',
|
|
9
|
+
description: 'Demo command',
|
|
10
|
+
strategy: Strategy.PUBLIC,
|
|
11
|
+
browser: false,
|
|
12
|
+
args: [
|
|
13
|
+
{
|
|
14
|
+
name: 'field',
|
|
15
|
+
help: 'Field to use',
|
|
16
|
+
choices: ['all-fields', 'topic', 'title', 'author', 'publication-titles', 'year-published', 'doi'],
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
columns: ['field'],
|
|
20
|
+
};
|
|
21
|
+
expect(formatRegistryHelpText(cmd)).toContain('--field: all-fields, topic, title, author, ... (+3 more)');
|
|
22
|
+
});
|
|
23
|
+
});
|
package/dist/tui.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Uses raw stdin mode + ANSI escape codes for interactive prompts.
|
|
5
5
|
*/
|
|
6
6
|
import chalk from 'chalk';
|
|
7
|
+
import { EXIT_CODES } from './errors.js';
|
|
7
8
|
/**
|
|
8
9
|
* Interactive multi-select checkbox prompt.
|
|
9
10
|
*
|
|
@@ -130,7 +131,7 @@ export async function checkboxPrompt(items, opts = {}) {
|
|
|
130
131
|
// Ctrl+C — exit process
|
|
131
132
|
if (key === '\x03') {
|
|
132
133
|
cleanup();
|
|
133
|
-
process.exit(
|
|
134
|
+
process.exit(EXIT_CODES.INTERRUPTED);
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
137
|
stdin.on('data', onData);
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
# 新浪财经 (Sina Finance)
|
|
2
2
|
|
|
3
|
-
**Mode**: 🌐 Public · **Domain**: `finance.sina.com.cn`
|
|
3
|
+
**Mode**: 🌐 Public / 🔐 Browser · **Domain**: `finance.sina.com.cn`
|
|
4
4
|
|
|
5
5
|
## Commands
|
|
6
6
|
|
|
7
|
-
| Command | Description |
|
|
8
|
-
|
|
9
|
-
| `opencli sinafinance news` | 新浪财经 7×24 小时实时快讯 |
|
|
7
|
+
| Command | Description | Mode |
|
|
8
|
+
|---------|-------------|------|
|
|
9
|
+
| `opencli sinafinance news` | 新浪财经 7×24 小时实时快讯 | 🌐 Public |
|
|
10
|
+
| `opencli sinafinance rolling-news` | 新浪财经滚动新闻 | 🔐 Browser |
|
|
11
|
+
| `opencli sinafinance stock` | 新浪财经行情(A股/港股/美股) | 🌐 Public |
|
|
10
12
|
|
|
11
13
|
## Usage Examples
|
|
12
14
|
|
|
15
|
+
### news - 7×24 实时快讯
|
|
16
|
+
|
|
13
17
|
```bash
|
|
14
18
|
# Latest financial news
|
|
15
19
|
opencli sinafinance news --limit 20
|
|
@@ -23,13 +27,59 @@ opencli sinafinance news --type 6 # 国际
|
|
|
23
27
|
opencli sinafinance news -f json
|
|
24
28
|
```
|
|
25
29
|
|
|
26
|
-
###
|
|
30
|
+
### rolling-news - 滚动新闻
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Rolling news feed
|
|
34
|
+
opencli sinafinance rolling-news
|
|
35
|
+
|
|
36
|
+
# JSON output
|
|
37
|
+
opencli sinafinance rolling-news -f json
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### stock - 股票行情
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Search and view A-share stock
|
|
44
|
+
opencli sinafinance stock 贵州茅台 --market cn
|
|
45
|
+
|
|
46
|
+
# Search and view HK stock
|
|
47
|
+
opencli sinafinance stock 腾讯控股 --market hk
|
|
48
|
+
|
|
49
|
+
# Search and view US stock
|
|
50
|
+
opencli sinafinance stock aapl --market us
|
|
51
|
+
|
|
52
|
+
# Auto-detect market (searches cn, hk, us in order)
|
|
53
|
+
opencli sinafinance stock 招商证券
|
|
54
|
+
|
|
55
|
+
# JSON output
|
|
56
|
+
opencli sinafinance stock 贵州茅台 -f json
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Options
|
|
60
|
+
|
|
61
|
+
### news
|
|
27
62
|
|
|
28
63
|
| Option | Description |
|
|
29
64
|
|--------|-------------|
|
|
30
65
|
| `--limit` | Max results, up to 50 (default: 20) |
|
|
31
66
|
| `--type` | News type: `0`=全部, `1`=A股, `2`=宏观, `3`=公司, `4`=数据, `5`=市场, `6`=国际, `7`=观点, `8`=央行, `9`=其它 |
|
|
32
67
|
|
|
68
|
+
### stock
|
|
69
|
+
|
|
70
|
+
| Option | Description |
|
|
71
|
+
|--------|-------------|
|
|
72
|
+
| `--market` | Market: `cn`, `hk`, `us`, `auto` (default: auto). When `auto`, searches in cn, hk, us order |
|
|
73
|
+
|
|
33
74
|
## Prerequisites
|
|
34
75
|
|
|
35
|
-
- No browser required — uses public API
|
|
76
|
+
- `news` & `stock`: No browser required — uses public API
|
|
77
|
+
- `rolling-news`: Chrome running and **logged into** `finance.sina.com.cn`
|
|
78
|
+
- For `rolling-news`: [Browser Bridge extension](/guide/browser-bridge) installed
|
|
79
|
+
|
|
80
|
+
## Notes
|
|
81
|
+
|
|
82
|
+
- `news` and `stock` use public APIs — no browser or login needed
|
|
83
|
+
- `stock` supports Chinese names, Chinese codes, and ticker symbols; auto-detects market
|
|
84
|
+
- Market priority for auto-detection: cn (A股) → hk (港股) → us (美股)
|
|
85
|
+
- US stock `High`/`Low` columns show 52-week range; A股/港股 show today's range
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const DAEMON_PORT = 19825;
|
|
2
2
|
const DAEMON_HOST = "localhost";
|
|
3
3
|
const DAEMON_WS_URL = `ws://${DAEMON_HOST}:${DAEMON_PORT}/ext`;
|
|
4
|
+
const DAEMON_PING_URL = `http://${DAEMON_HOST}:${DAEMON_PORT}/ping`;
|
|
4
5
|
const WS_RECONNECT_BASE_DELAY = 2e3;
|
|
5
6
|
const WS_RECONNECT_MAX_DELAY = 6e4;
|
|
6
7
|
|
|
@@ -149,8 +150,14 @@ console.error = (...args) => {
|
|
|
149
150
|
_origError(...args);
|
|
150
151
|
forwardLog("error", args);
|
|
151
152
|
};
|
|
152
|
-
function connect() {
|
|
153
|
+
async function connect() {
|
|
153
154
|
if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) return;
|
|
155
|
+
try {
|
|
156
|
+
const res = await fetch(DAEMON_PING_URL, { signal: AbortSignal.timeout(1e3) });
|
|
157
|
+
if (!res.ok) return;
|
|
158
|
+
} catch {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
154
161
|
try {
|
|
155
162
|
ws = new WebSocket(DAEMON_WS_URL);
|
|
156
163
|
} catch {
|
|
@@ -192,7 +199,7 @@ function scheduleReconnect() {
|
|
|
192
199
|
const delay = Math.min(WS_RECONNECT_BASE_DELAY * Math.pow(2, reconnectAttempts - 1), WS_RECONNECT_MAX_DELAY);
|
|
193
200
|
reconnectTimer = setTimeout(() => {
|
|
194
201
|
reconnectTimer = null;
|
|
195
|
-
connect();
|
|
202
|
+
void connect();
|
|
196
203
|
}, delay);
|
|
197
204
|
}
|
|
198
205
|
const automationSessions = /* @__PURE__ */ new Map();
|
|
@@ -231,8 +238,7 @@ async function getAutomationWindow(workspace) {
|
|
|
231
238
|
focused: false,
|
|
232
239
|
width: 1280,
|
|
233
240
|
height: 900,
|
|
234
|
-
type: "normal"
|
|
235
|
-
state: "normal"
|
|
241
|
+
type: "normal"
|
|
236
242
|
});
|
|
237
243
|
const session = {
|
|
238
244
|
windowId: win.id,
|
|
@@ -260,7 +266,7 @@ function initialize() {
|
|
|
260
266
|
initialized = true;
|
|
261
267
|
chrome.alarms.create("keepalive", { periodInMinutes: 0.4 });
|
|
262
268
|
registerListeners();
|
|
263
|
-
connect();
|
|
269
|
+
void connect();
|
|
264
270
|
console.log("[opencli] OpenCLI extension initialized");
|
|
265
271
|
}
|
|
266
272
|
chrome.runtime.onInstalled.addListener(() => {
|
|
@@ -270,7 +276,7 @@ chrome.runtime.onStartup.addListener(() => {
|
|
|
270
276
|
initialize();
|
|
271
277
|
});
|
|
272
278
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
273
|
-
if (alarm.name === "keepalive") connect();
|
|
279
|
+
if (alarm.name === "keepalive") void connect();
|
|
274
280
|
});
|
|
275
281
|
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
|
276
282
|
if (msg?.type === "getStatus") {
|
package/extension/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "OpenCLI",
|
|
4
|
-
"version": "1.5.
|
|
4
|
+
"version": "1.5.5",
|
|
5
5
|
"description": "Browser automation bridge for the OpenCLI CLI tool. Executes commands in isolated Chrome windows via a local daemon.",
|
|
6
6
|
"permissions": [
|
|
7
7
|
"debugger",
|
|
@@ -35,4 +35,4 @@
|
|
|
35
35
|
"extension_pages": "script-src 'self'; object-src 'self'"
|
|
36
36
|
},
|
|
37
37
|
"homepage_url": "https://github.com/jackwener/opencli"
|
|
38
|
-
}
|
|
38
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencli-extension",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.4",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "opencli-extension",
|
|
9
|
-
"version": "1.5.
|
|
9
|
+
"version": "1.5.4",
|
|
10
10
|
"devDependencies": {
|
|
11
11
|
"@types/chrome": "^0.0.287",
|
|
12
12
|
"typescript": "^5.7.0",
|
package/extension/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { Command, Result } from './protocol';
|
|
9
|
-
import { DAEMON_WS_URL, WS_RECONNECT_BASE_DELAY, WS_RECONNECT_MAX_DELAY } from './protocol';
|
|
9
|
+
import { DAEMON_WS_URL, DAEMON_PING_URL, WS_RECONNECT_BASE_DELAY, WS_RECONNECT_MAX_DELAY } from './protocol';
|
|
10
10
|
import * as executor from './cdp';
|
|
11
11
|
|
|
12
12
|
let ws: WebSocket | null = null;
|
|
@@ -34,9 +34,23 @@ console.error = (...args: unknown[]) => { _origError(...args); forwardLog('error
|
|
|
34
34
|
|
|
35
35
|
// ─── WebSocket connection ────────────────────────────────────────────
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Probe the daemon via its /ping HTTP endpoint before attempting a WebSocket
|
|
39
|
+
* connection. fetch() failures are silently catchable; new WebSocket() is not
|
|
40
|
+
* — Chrome logs ERR_CONNECTION_REFUSED to the extension error page before any
|
|
41
|
+
* JS handler can intercept it. By keeping the probe inside connect() every
|
|
42
|
+
* call site remains unchanged and the guard can never be accidentally skipped.
|
|
43
|
+
*/
|
|
44
|
+
async function connect(): Promise<void> {
|
|
38
45
|
if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) return;
|
|
39
46
|
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch(DAEMON_PING_URL, { signal: AbortSignal.timeout(1000) });
|
|
49
|
+
if (!res.ok) return; // unexpected response — not our daemon
|
|
50
|
+
} catch {
|
|
51
|
+
return; // daemon not running — skip WebSocket to avoid console noise
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
try {
|
|
41
55
|
ws = new WebSocket(DAEMON_WS_URL);
|
|
42
56
|
} catch {
|
|
@@ -90,7 +104,7 @@ function scheduleReconnect(): void {
|
|
|
90
104
|
const delay = Math.min(WS_RECONNECT_BASE_DELAY * Math.pow(2, reconnectAttempts - 1), WS_RECONNECT_MAX_DELAY);
|
|
91
105
|
reconnectTimer = setTimeout(() => {
|
|
92
106
|
reconnectTimer = null;
|
|
93
|
-
connect();
|
|
107
|
+
void connect();
|
|
94
108
|
}, delay);
|
|
95
109
|
}
|
|
96
110
|
|
|
@@ -146,13 +160,14 @@ async function getAutomationWindow(workspace: string): Promise<number> {
|
|
|
146
160
|
|
|
147
161
|
// Create a new window with a data: URI that New Tab Override extensions cannot intercept.
|
|
148
162
|
// Using about:blank would be hijacked by extensions like "New Tab Override".
|
|
163
|
+
// Note: Do NOT set `state` parameter here. Chrome 146+ rejects 'normal' as an invalid
|
|
164
|
+
// state value for windows.create(). The window defaults to 'normal' state anyway.
|
|
149
165
|
const win = await chrome.windows.create({
|
|
150
166
|
url: BLANK_PAGE,
|
|
151
167
|
focused: false,
|
|
152
168
|
width: 1280,
|
|
153
169
|
height: 900,
|
|
154
170
|
type: 'normal',
|
|
155
|
-
state: 'normal',
|
|
156
171
|
});
|
|
157
172
|
const session: AutomationSession = {
|
|
158
173
|
windowId: win.id!,
|
|
@@ -187,7 +202,7 @@ function initialize(): void {
|
|
|
187
202
|
initialized = true;
|
|
188
203
|
chrome.alarms.create('keepalive', { periodInMinutes: 0.4 }); // ~24 seconds
|
|
189
204
|
executor.registerListeners();
|
|
190
|
-
connect();
|
|
205
|
+
void connect();
|
|
191
206
|
console.log('[opencli] OpenCLI extension initialized');
|
|
192
207
|
}
|
|
193
208
|
|
|
@@ -200,7 +215,7 @@ chrome.runtime.onStartup.addListener(() => {
|
|
|
200
215
|
});
|
|
201
216
|
|
|
202
217
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
203
|
-
if (alarm.name === 'keepalive') connect();
|
|
218
|
+
if (alarm.name === 'keepalive') void connect();
|
|
204
219
|
});
|
|
205
220
|
|
|
206
221
|
// ─── Popup status API ───────────────────────────────────────────────
|
|
@@ -49,7 +49,8 @@ export interface Result {
|
|
|
49
49
|
export const DAEMON_PORT = 19825;
|
|
50
50
|
export const DAEMON_HOST = 'localhost';
|
|
51
51
|
export const DAEMON_WS_URL = `ws://${DAEMON_HOST}:${DAEMON_PORT}/ext`;
|
|
52
|
-
|
|
52
|
+
/** Lightweight health-check endpoint — probed before each WebSocket attempt. */
|
|
53
|
+
export const DAEMON_PING_URL = `http://${DAEMON_HOST}:${DAEMON_PORT}/ping`;
|
|
53
54
|
|
|
54
55
|
/** Base reconnect delay for extension WebSocket (ms) */
|
|
55
56
|
export const WS_RECONNECT_BASE_DELAY = 2000;
|
package/package.json
CHANGED
|
@@ -2,69 +2,8 @@ import { afterEach, describe, expect, it } from 'vitest';
|
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as os from 'node:os';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
describe('parseTsArgsBlock', () => {
|
|
8
|
-
it('keeps args with nested choices arrays', () => {
|
|
9
|
-
const args = parseTsArgsBlock(`
|
|
10
|
-
{
|
|
11
|
-
name: 'period',
|
|
12
|
-
type: 'string',
|
|
13
|
-
default: 'seven',
|
|
14
|
-
help: 'Stats period: seven or thirty',
|
|
15
|
-
choices: ['seven', 'thirty'],
|
|
16
|
-
},
|
|
17
|
-
`);
|
|
18
|
-
|
|
19
|
-
expect(args).toEqual([
|
|
20
|
-
{
|
|
21
|
-
name: 'period',
|
|
22
|
-
type: 'string',
|
|
23
|
-
default: 'seven',
|
|
24
|
-
required: false,
|
|
25
|
-
positional: undefined,
|
|
26
|
-
help: 'Stats period: seven or thirty',
|
|
27
|
-
choices: ['seven', 'thirty'],
|
|
28
|
-
},
|
|
29
|
-
]);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('keeps hyphenated arg names from TS adapters', () => {
|
|
33
|
-
const args = parseTsArgsBlock(`
|
|
34
|
-
{
|
|
35
|
-
name: 'tweet-url',
|
|
36
|
-
help: 'Single tweet URL to download',
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
name: 'download-images',
|
|
40
|
-
type: 'boolean',
|
|
41
|
-
default: false,
|
|
42
|
-
help: 'Download images locally',
|
|
43
|
-
},
|
|
44
|
-
`);
|
|
45
|
-
|
|
46
|
-
expect(args).toEqual([
|
|
47
|
-
{
|
|
48
|
-
name: 'tweet-url',
|
|
49
|
-
type: 'str',
|
|
50
|
-
default: undefined,
|
|
51
|
-
required: false,
|
|
52
|
-
positional: undefined,
|
|
53
|
-
help: 'Single tweet URL to download',
|
|
54
|
-
choices: undefined,
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
name: 'download-images',
|
|
58
|
-
type: 'boolean',
|
|
59
|
-
default: false,
|
|
60
|
-
required: false,
|
|
61
|
-
positional: undefined,
|
|
62
|
-
help: 'Download images locally',
|
|
63
|
-
choices: undefined,
|
|
64
|
-
},
|
|
65
|
-
]);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
5
|
+
import { cli, getRegistry, Strategy } from './registry.js';
|
|
6
|
+
import { loadTsManifestEntries, shouldReplaceManifestEntry } from './build-manifest.js';
|
|
68
7
|
|
|
69
8
|
describe('manifest helper rules', () => {
|
|
70
9
|
const tempDirs: string[] = [];
|
|
@@ -127,43 +66,133 @@ describe('manifest helper rules', () => {
|
|
|
127
66
|
const file = path.join(dir, 'utils.ts');
|
|
128
67
|
fs.writeFileSync(file, `export function helper() { return 'noop'; }`);
|
|
129
68
|
|
|
130
|
-
expect(
|
|
69
|
+
return expect(loadTsManifestEntries(file, 'demo', async () => ({}))).resolves.toEqual([]);
|
|
131
70
|
});
|
|
132
71
|
|
|
133
|
-
it('
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
72
|
+
it('builds TS manifest entries from exported runtime commands', async () => {
|
|
73
|
+
const site = `manifest-hydrate-${Date.now()}`;
|
|
74
|
+
const key = `${site}/dynamic`;
|
|
75
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-manifest-'));
|
|
76
|
+
tempDirs.push(dir);
|
|
77
|
+
const file = path.join(dir, `${site}.ts`);
|
|
78
|
+
fs.writeFileSync(file, `export const command = cli({ site: '${site}', name: 'dynamic' });`);
|
|
79
|
+
|
|
80
|
+
const entries = await loadTsManifestEntries(file, site, async () => ({
|
|
81
|
+
command: cli({
|
|
82
|
+
site,
|
|
83
|
+
name: 'dynamic',
|
|
84
|
+
description: 'dynamic command',
|
|
85
|
+
strategy: Strategy.PUBLIC,
|
|
86
|
+
browser: false,
|
|
87
|
+
args: [
|
|
88
|
+
{
|
|
89
|
+
name: 'model',
|
|
90
|
+
required: true,
|
|
91
|
+
positional: true,
|
|
92
|
+
help: 'Choose a model',
|
|
93
|
+
choices: ['auto', 'thinking'],
|
|
94
|
+
default: '30',
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
domain: 'localhost',
|
|
98
|
+
navigateBefore: 'https://example.com/session',
|
|
99
|
+
deprecated: 'legacy command',
|
|
100
|
+
replacedBy: 'opencli demo new',
|
|
101
|
+
}),
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
expect(entries).toEqual([
|
|
105
|
+
{
|
|
106
|
+
site,
|
|
107
|
+
name: 'dynamic',
|
|
108
|
+
description: 'dynamic command',
|
|
109
|
+
domain: 'localhost',
|
|
110
|
+
strategy: 'public',
|
|
111
|
+
browser: false,
|
|
112
|
+
args: [
|
|
113
|
+
{
|
|
114
|
+
name: 'model',
|
|
115
|
+
type: 'str',
|
|
116
|
+
required: true,
|
|
117
|
+
positional: true,
|
|
118
|
+
help: 'Choose a model',
|
|
119
|
+
choices: ['auto', 'thinking'],
|
|
120
|
+
default: '30',
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
type: 'ts',
|
|
124
|
+
modulePath: `${site}/${site}.js`,
|
|
125
|
+
navigateBefore: 'https://example.com/session',
|
|
126
|
+
deprecated: 'legacy command',
|
|
127
|
+
replacedBy: 'opencli demo new',
|
|
128
|
+
},
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
getRegistry().delete(key);
|
|
145
132
|
});
|
|
146
133
|
|
|
147
|
-
it('
|
|
134
|
+
it('falls back to registry delta for side-effect-only cli modules', async () => {
|
|
135
|
+
const site = `manifest-side-effect-${Date.now()}`;
|
|
136
|
+
const key = `${site}/legacy`;
|
|
148
137
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-manifest-'));
|
|
149
138
|
tempDirs.push(dir);
|
|
150
|
-
const file = path.join(dir,
|
|
151
|
-
fs.writeFileSync(file, `
|
|
152
|
-
|
|
139
|
+
const file = path.join(dir, `${site}.ts`);
|
|
140
|
+
fs.writeFileSync(file, `cli({ site: '${site}', name: 'legacy' });`);
|
|
141
|
+
|
|
142
|
+
const entries = await loadTsManifestEntries(file, site, async () => {
|
|
153
143
|
cli({
|
|
154
|
-
site
|
|
144
|
+
site,
|
|
155
145
|
name: 'legacy',
|
|
156
146
|
description: 'legacy command',
|
|
157
147
|
deprecated: 'legacy is deprecated',
|
|
158
148
|
replacedBy: 'opencli demo new',
|
|
159
149
|
});
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
expect(scanTs(file, 'demo')).toMatchObject({
|
|
163
|
-
site: 'demo',
|
|
164
|
-
name: 'legacy',
|
|
165
|
-
deprecated: 'legacy is deprecated',
|
|
166
|
-
replacedBy: 'opencli demo new',
|
|
150
|
+
return {};
|
|
167
151
|
});
|
|
152
|
+
|
|
153
|
+
expect(entries).toEqual([
|
|
154
|
+
{
|
|
155
|
+
site,
|
|
156
|
+
name: 'legacy',
|
|
157
|
+
description: 'legacy command',
|
|
158
|
+
strategy: 'cookie',
|
|
159
|
+
browser: true,
|
|
160
|
+
args: [],
|
|
161
|
+
type: 'ts',
|
|
162
|
+
modulePath: `${site}/${site}.js`,
|
|
163
|
+
deprecated: 'legacy is deprecated',
|
|
164
|
+
replacedBy: 'opencli demo new',
|
|
165
|
+
},
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
getRegistry().delete(key);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('keeps every command a module exports instead of guessing by site', async () => {
|
|
172
|
+
const site = `manifest-multi-${Date.now()}`;
|
|
173
|
+
const screenKey = `${site}/screen`;
|
|
174
|
+
const statusKey = `${site}/status`;
|
|
175
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-manifest-'));
|
|
176
|
+
tempDirs.push(dir);
|
|
177
|
+
const file = path.join(dir, `${site}.ts`);
|
|
178
|
+
fs.writeFileSync(file, `export const screen = cli({ site: '${site}', name: 'screen' });`);
|
|
179
|
+
|
|
180
|
+
const entries = await loadTsManifestEntries(file, site, async () => ({
|
|
181
|
+
screen: cli({
|
|
182
|
+
site,
|
|
183
|
+
name: 'screen',
|
|
184
|
+
description: 'capture screen',
|
|
185
|
+
}),
|
|
186
|
+
status: cli({
|
|
187
|
+
site,
|
|
188
|
+
name: 'status',
|
|
189
|
+
description: 'show status',
|
|
190
|
+
}),
|
|
191
|
+
}));
|
|
192
|
+
|
|
193
|
+
expect(entries.map(entry => entry.name)).toEqual(['screen', 'status']);
|
|
194
|
+
|
|
195
|
+
getRegistry().delete(screenKey);
|
|
196
|
+
getRegistry().delete(statusKey);
|
|
168
197
|
});
|
|
169
198
|
});
|