@jackwener/opencli 1.5.3 → 1.5.4
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 +1199 -1106
- package/dist/daemon.js +14 -3
- package/dist/external-clis.yaml +16 -0
- package/dist/serialization.js +6 -1
- package/dist/serialization.test.d.ts +1 -0
- package/dist/serialization.test.js +23 -0
- package/extension/dist/background.js +11 -4
- package/extension/manifest.json +2 -2
- package/extension/package-lock.json +2 -2
- package/extension/package.json +1 -1
- package/extension/src/background.ts +19 -5
- 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/daemon.ts +16 -4
- package/src/external-clis.yaml +16 -0
- package/src/serialization.test.ts +26 -0
- package/src/serialization.ts +6 -1
package/dist/daemon.js
CHANGED
|
@@ -90,7 +90,20 @@ async function handleRequest(req, res) {
|
|
|
90
90
|
res.end();
|
|
91
91
|
return;
|
|
92
92
|
}
|
|
93
|
-
|
|
93
|
+
const url = req.url ?? '/';
|
|
94
|
+
const pathname = url.split('?')[0];
|
|
95
|
+
// Health-check endpoint — no X-OpenCLI header required.
|
|
96
|
+
// Used by the extension to silently probe daemon reachability before
|
|
97
|
+
// attempting a WebSocket connection (avoids uncatchable ERR_CONNECTION_REFUSED).
|
|
98
|
+
// Security note: this endpoint is reachable by any client that passes the
|
|
99
|
+
// origin check above (chrome-extension:// or no Origin header, e.g. curl).
|
|
100
|
+
// Timing side-channels can reveal daemon presence to local processes, which
|
|
101
|
+
// is an accepted risk given the daemon is loopback-only and short-lived.
|
|
102
|
+
if (req.method === 'GET' && pathname === '/ping') {
|
|
103
|
+
jsonResponse(res, 200, { ok: true });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// Require custom header on all other HTTP requests. Browsers cannot attach
|
|
94
107
|
// custom headers in "simple" requests, and our preflight returns no
|
|
95
108
|
// Access-Control-Allow-Headers, so scripted fetch() from web pages is
|
|
96
109
|
// blocked even if Origin check is somehow bypassed.
|
|
@@ -98,8 +111,6 @@ async function handleRequest(req, res) {
|
|
|
98
111
|
jsonResponse(res, 403, { ok: false, error: 'Forbidden: missing X-OpenCLI header' });
|
|
99
112
|
return;
|
|
100
113
|
}
|
|
101
|
-
const url = req.url ?? '/';
|
|
102
|
-
const pathname = url.split('?')[0];
|
|
103
114
|
if (req.method === 'GET' && pathname === '/status') {
|
|
104
115
|
jsonResponse(res, 200, {
|
|
105
116
|
ok: true,
|
package/dist/external-clis.yaml
CHANGED
|
@@ -21,3 +21,19 @@
|
|
|
21
21
|
tags: [docker, containers, devops]
|
|
22
22
|
install:
|
|
23
23
|
mac: "brew install --cask docker"
|
|
24
|
+
|
|
25
|
+
- name: lark-cli
|
|
26
|
+
binary: lark-cli
|
|
27
|
+
description: "Lark/Feishu CLI — messages, documents, spreadsheets, calendar, tasks and 200+ commands for AI agents"
|
|
28
|
+
homepage: "https://github.com/larksuite/cli"
|
|
29
|
+
tags: [lark, feishu, collaboration, productivity, ai-agent]
|
|
30
|
+
install:
|
|
31
|
+
default: "npm install -g @larksuite/cli"
|
|
32
|
+
|
|
33
|
+
- name: vercel
|
|
34
|
+
binary: vercel
|
|
35
|
+
description: "Vercel CLI — deploy projects, manage domains, env vars, logs and serverless functions"
|
|
36
|
+
homepage: "https://vercel.com/docs/cli"
|
|
37
|
+
tags: [vercel, deployment, serverless, frontend, devops]
|
|
38
|
+
install:
|
|
39
|
+
default: "npm install -g vercel"
|
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
|
+
});
|
|
@@ -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();
|
|
@@ -260,7 +267,7 @@ function initialize() {
|
|
|
260
267
|
initialized = true;
|
|
261
268
|
chrome.alarms.create("keepalive", { periodInMinutes: 0.4 });
|
|
262
269
|
registerListeners();
|
|
263
|
-
connect();
|
|
270
|
+
void connect();
|
|
264
271
|
console.log("[opencli] OpenCLI extension initialized");
|
|
265
272
|
}
|
|
266
273
|
chrome.runtime.onInstalled.addListener(() => {
|
|
@@ -270,7 +277,7 @@ chrome.runtime.onStartup.addListener(() => {
|
|
|
270
277
|
initialize();
|
|
271
278
|
});
|
|
272
279
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
273
|
-
if (alarm.name === "keepalive") connect();
|
|
280
|
+
if (alarm.name === "keepalive") void connect();
|
|
274
281
|
});
|
|
275
282
|
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
|
276
283
|
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.4",
|
|
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
|
|
|
@@ -187,7 +201,7 @@ function initialize(): void {
|
|
|
187
201
|
initialized = true;
|
|
188
202
|
chrome.alarms.create('keepalive', { periodInMinutes: 0.4 }); // ~24 seconds
|
|
189
203
|
executor.registerListeners();
|
|
190
|
-
connect();
|
|
204
|
+
void connect();
|
|
191
205
|
console.log('[opencli] OpenCLI extension initialized');
|
|
192
206
|
}
|
|
193
207
|
|
|
@@ -200,7 +214,7 @@ chrome.runtime.onStartup.addListener(() => {
|
|
|
200
214
|
});
|
|
201
215
|
|
|
202
216
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
203
|
-
if (alarm.name === 'keepalive') connect();
|
|
217
|
+
if (alarm.name === 'keepalive') void connect();
|
|
204
218
|
});
|
|
205
219
|
|
|
206
220
|
// ─── 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
|
});
|