@jackwener/opencli 1.7.3 → 1.7.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/README.md +81 -59
- package/README.zh-CN.md +93 -67
- package/cli-manifest.json +5015 -2975
- package/clis/antigravity/serve.js +71 -25
- package/clis/baidu-scholar/search.js +87 -0
- package/clis/baidu-scholar/search.test.js +23 -0
- package/clis/bilibili/favorite.js +18 -13
- package/clis/binance/depth.js +3 -4
- package/clis/boss/utils.js +2 -3
- package/clis/chatgpt-app/ax.js +6 -3
- package/clis/deepseek/ask.js +74 -0
- package/clis/deepseek/history.js +25 -0
- package/clis/deepseek/new.js +20 -0
- package/clis/deepseek/read.js +22 -0
- package/clis/deepseek/status.js +24 -0
- package/clis/deepseek/utils.js +208 -0
- package/clis/douban/search.js +1 -0
- package/clis/douban/search.test.js +11 -0
- package/clis/douban/subject.js +20 -93
- package/clis/douban/subject.test.js +11 -0
- package/clis/douban/utils.js +250 -8
- package/clis/douban/utils.test.js +179 -4
- package/clis/doubao/utils.js +319 -130
- package/clis/doubao/utils.test.js +241 -2
- package/clis/eastmoney/_secid.js +78 -0
- package/clis/eastmoney/announcement.js +52 -0
- package/clis/eastmoney/convertible.js +73 -0
- package/clis/eastmoney/etf.js +65 -0
- package/clis/eastmoney/holders.js +78 -0
- package/clis/eastmoney/hot-rank.js +50 -0
- package/clis/eastmoney/hot-rank.test.js +59 -0
- package/clis/eastmoney/index-board.js +96 -0
- package/clis/eastmoney/kline.js +87 -0
- package/clis/eastmoney/kuaixun.js +54 -0
- package/clis/eastmoney/longhu.js +67 -0
- package/clis/eastmoney/money-flow.js +78 -0
- package/clis/eastmoney/northbound.js +57 -0
- package/clis/eastmoney/quote.js +107 -0
- package/clis/eastmoney/rank.js +94 -0
- package/clis/eastmoney/sectors.js +76 -0
- package/clis/google-scholar/search.js +58 -0
- package/clis/google-scholar/search.test.js +23 -0
- package/clis/gov-law/commands.test.js +39 -0
- package/clis/gov-law/recent.js +22 -0
- package/clis/gov-law/search.js +41 -0
- package/clis/gov-law/shared.js +51 -0
- package/clis/gov-policy/commands.test.js +27 -0
- package/clis/gov-policy/recent.js +47 -0
- package/clis/gov-policy/search.js +48 -0
- package/clis/grok/image.test.ts +107 -0
- package/clis/grok/image.ts +356 -0
- package/clis/nowcoder/companies.js +23 -0
- package/clis/nowcoder/creators.js +27 -0
- package/clis/nowcoder/detail.js +61 -0
- package/clis/nowcoder/experience.js +36 -0
- package/clis/nowcoder/hot.js +24 -0
- package/clis/nowcoder/jobs.js +21 -0
- package/clis/nowcoder/notifications.js +29 -0
- package/clis/nowcoder/papers.js +40 -0
- package/clis/nowcoder/practice.js +37 -0
- package/clis/nowcoder/recommend.js +30 -0
- package/clis/nowcoder/referral.js +39 -0
- package/clis/nowcoder/salary.js +40 -0
- package/clis/nowcoder/search.js +49 -0
- package/clis/nowcoder/suggest.js +33 -0
- package/clis/nowcoder/topics.js +27 -0
- package/clis/nowcoder/trending.js +25 -0
- package/clis/tdx/hot-rank.js +47 -0
- package/clis/tdx/hot-rank.test.js +59 -0
- package/clis/ths/hot-rank.js +49 -0
- package/clis/ths/hot-rank.test.js +64 -0
- package/clis/twitter/bookmarks.js +2 -1
- package/clis/twitter/list-add.js +337 -0
- package/clis/twitter/list-add.test.js +15 -0
- package/clis/twitter/list-remove.js +297 -0
- package/clis/twitter/list-remove.test.js +14 -0
- package/clis/twitter/list-tweets.js +185 -0
- package/clis/twitter/list-tweets.test.js +108 -0
- package/clis/twitter/lists.js +134 -47
- package/clis/twitter/lists.test.js +105 -38
- package/clis/uiverse/_shared.js +368 -0
- package/clis/uiverse/_shared.test.js +55 -0
- package/clis/uiverse/code.js +47 -0
- package/clis/uiverse/preview.js +71 -0
- package/clis/wanfang/search.js +66 -0
- package/clis/wanfang/search.test.js +23 -0
- package/clis/web/read.js +1 -1
- package/clis/weixin/download.js +3 -2
- package/clis/xiaohongshu/comments.js +2 -2
- package/clis/xiaohongshu/comments.test.js +46 -25
- package/clis/xiaohongshu/download.js +6 -7
- package/clis/xiaohongshu/download.test.js +17 -5
- package/clis/xiaohongshu/note-helpers.js +46 -12
- package/clis/xiaohongshu/note.js +3 -5
- package/clis/xiaohongshu/note.test.js +52 -25
- package/clis/xiaohongshu/publish.js +149 -28
- package/clis/xiaohongshu/publish.test.js +319 -6
- package/clis/xiaoyuzhou/auth.js +303 -0
- package/clis/xiaoyuzhou/auth.test.js +124 -0
- package/clis/xiaoyuzhou/download.js +53 -0
- package/clis/xiaoyuzhou/download.test.js +135 -0
- package/clis/xiaoyuzhou/episode.js +9 -4
- package/clis/xiaoyuzhou/podcast-episodes.js +15 -11
- package/clis/xiaoyuzhou/podcast.js +9 -4
- package/clis/xiaoyuzhou/transcript.js +76 -0
- package/clis/xiaoyuzhou/transcript.test.js +195 -0
- package/clis/xiaoyuzhou/utils.js +0 -40
- package/clis/xiaoyuzhou/utils.test.js +15 -75
- package/clis/youtube/feed.js +120 -0
- package/clis/youtube/history.js +118 -0
- package/clis/youtube/like.js +62 -0
- package/clis/youtube/playlist.js +97 -0
- package/clis/youtube/subscribe.js +71 -0
- package/clis/youtube/subscriptions.js +57 -0
- package/clis/youtube/unlike.js +62 -0
- package/clis/youtube/unsubscribe.js +71 -0
- package/clis/youtube/utils.js +122 -0
- package/clis/youtube/utils.test.js +32 -1
- package/clis/youtube/watch-later.js +76 -0
- package/clis/zsxq/dynamics.js +1 -1
- package/clis/zsxq/utils.js +6 -3
- package/clis/zsxq/utils.test.js +31 -0
- package/dist/src/browser/base-page.d.ts +1 -1
- package/dist/src/browser/base-page.js +25 -5
- package/dist/src/browser/bridge.d.ts +3 -0
- package/dist/src/browser/bridge.js +52 -15
- package/dist/src/browser/cdp.js +2 -1
- package/dist/src/browser/daemon-client.d.ts +7 -4
- package/dist/src/browser/daemon-client.js +6 -1
- package/dist/src/browser/daemon-client.test.js +40 -1
- package/dist/src/browser/dom-snapshot.js +20 -3
- package/dist/src/browser/page.d.ts +18 -5
- package/dist/src/browser/page.js +96 -15
- package/dist/src/browser/page.test.js +158 -1
- package/dist/src/browser/target-errors.d.ts +23 -0
- package/dist/src/browser/target-errors.js +29 -0
- package/dist/src/browser/target-errors.test.js +61 -0
- package/dist/src/browser/target-resolver.d.ts +57 -0
- package/dist/src/browser/target-resolver.js +298 -0
- package/dist/src/browser/target-resolver.test.js +43 -0
- package/dist/src/browser.test.js +38 -1
- package/dist/src/cli.js +272 -187
- package/dist/src/cli.test.js +167 -90
- package/dist/src/commanderAdapter.d.ts +0 -1
- package/dist/src/commanderAdapter.js +2 -16
- package/dist/src/commanderAdapter.test.js +1 -1
- package/dist/src/commands/daemon.d.ts +4 -2
- package/dist/src/commands/daemon.js +22 -2
- package/dist/src/commands/daemon.test.js +65 -2
- package/dist/src/completion-shared.js +2 -5
- package/dist/src/daemon.js +10 -0
- package/dist/src/doctor.d.ts +1 -0
- package/dist/src/doctor.js +32 -9
- package/dist/src/doctor.test.js +28 -12
- package/dist/src/download/article-download.d.ts +1 -0
- package/dist/src/download/article-download.js +3 -0
- package/dist/src/download/article-download.test.js +39 -0
- package/dist/src/external-clis.yaml +2 -2
- package/dist/src/logger.d.ts +2 -2
- package/dist/src/logger.js +3 -3
- package/dist/src/output.js +1 -5
- package/dist/src/output.test.js +0 -21
- package/dist/src/pipeline/steps/transform.js +1 -1
- package/dist/src/pipeline/template.d.ts +1 -0
- package/dist/src/pipeline/template.js +11 -3
- package/dist/src/pipeline/template.test.js +3 -0
- package/dist/src/pipeline/transform.test.js +14 -0
- package/dist/src/plugin.d.ts +8 -9
- package/dist/src/plugin.js +24 -28
- package/dist/src/plugin.test.js +16 -60
- package/dist/src/registry.d.ts +1 -0
- package/dist/src/registry.js +3 -2
- package/dist/src/registry.test.js +22 -0
- package/dist/src/types.d.ts +15 -6
- package/package.json +1 -1
- package/clis/twitter/lists-parser.js +0 -77
- package/clis/twitter/lists.d.ts +0 -5
- package/dist/src/cascade.d.ts +0 -46
- package/dist/src/cascade.js +0 -135
- package/dist/src/explore.d.ts +0 -99
- package/dist/src/explore.js +0 -402
- package/dist/src/generate-verified.d.ts +0 -105
- package/dist/src/generate-verified.js +0 -696
- package/dist/src/generate-verified.test.js +0 -925
- package/dist/src/generate.d.ts +0 -46
- package/dist/src/generate.js +0 -117
- package/dist/src/record.d.ts +0 -96
- package/dist/src/record.js +0 -657
- package/dist/src/record.test.js +0 -293
- package/dist/src/skill-generate.d.ts +0 -30
- package/dist/src/skill-generate.js +0 -75
- package/dist/src/skill-generate.test.js +0 -173
- package/dist/src/synthesize.d.ts +0 -97
- package/dist/src/synthesize.js +0 -208
- /package/dist/src/{generate-verified.test.d.ts → browser/target-errors.test.d.ts} +0 -0
- /package/dist/src/{record.test.d.ts → browser/target-resolver.test.d.ts} +0 -0
- /package/dist/src/{skill-generate.test.d.ts → download/article-download.test.d.ts} +0 -0
package/dist/src/doctor.test.js
CHANGED
|
@@ -25,10 +25,11 @@ describe('doctor report rendering', () => {
|
|
|
25
25
|
const text = strip(renderBrowserDoctorReport({
|
|
26
26
|
daemonRunning: true,
|
|
27
27
|
extensionConnected: true,
|
|
28
|
+
extensionVersion: '1.6.8',
|
|
28
29
|
issues: [],
|
|
29
30
|
}));
|
|
30
31
|
expect(text).toContain('[OK] Daemon: running on port 19825');
|
|
31
|
-
expect(text).toContain('[OK] Extension: connected');
|
|
32
|
+
expect(text).toContain('[OK] Extension: connected (v1.6.8)');
|
|
32
33
|
expect(text).toContain('Everything looks good!');
|
|
33
34
|
});
|
|
34
35
|
it('renders MISSING when daemon not running', () => {
|
|
@@ -50,6 +51,16 @@ describe('doctor report rendering', () => {
|
|
|
50
51
|
expect(text).toContain('[OK] Daemon: running on port 19825');
|
|
51
52
|
expect(text).toContain('[MISSING] Extension: not connected');
|
|
52
53
|
});
|
|
54
|
+
it('renders a warning when the extension version is unknown', () => {
|
|
55
|
+
const text = strip(renderBrowserDoctorReport({
|
|
56
|
+
daemonRunning: true,
|
|
57
|
+
extensionConnected: true,
|
|
58
|
+
issues: ['Extension is connected but did not report a version.'],
|
|
59
|
+
}));
|
|
60
|
+
expect(text).toContain('[WARN] Extension: connected (version unknown)');
|
|
61
|
+
expect(text).toContain('Extension is connected but did not report a version.');
|
|
62
|
+
expect(text).not.toContain('Everything looks good!');
|
|
63
|
+
});
|
|
53
64
|
it('renders connectivity OK when live test succeeds', () => {
|
|
54
65
|
const text = strip(renderBrowserDoctorReport({
|
|
55
66
|
daemonRunning: true,
|
|
@@ -90,12 +101,8 @@ describe('doctor report rendering', () => {
|
|
|
90
101
|
expect(text).toContain('Daemon connectivity is unstable.');
|
|
91
102
|
});
|
|
92
103
|
it('reports daemon not running when no-live and auto-start fails', async () => {
|
|
93
|
-
// no-live mode: getDaemonHealth called twice (initial check + final status)
|
|
94
|
-
// Initial: stopped → triggers auto-start attempt
|
|
95
104
|
mockGetDaemonHealth.mockResolvedValueOnce({ state: 'stopped', status: null });
|
|
96
|
-
// Auto-start fails
|
|
97
105
|
mockConnect.mockRejectedValueOnce(new Error('Could not start daemon'));
|
|
98
|
-
// Final: still stopped
|
|
99
106
|
mockGetDaemonHealth.mockResolvedValueOnce({ state: 'stopped', status: null });
|
|
100
107
|
const report = await runBrowserDoctor({ live: false });
|
|
101
108
|
expect(report.daemonRunning).toBe(false);
|
|
@@ -106,12 +113,10 @@ describe('doctor report rendering', () => {
|
|
|
106
113
|
]));
|
|
107
114
|
});
|
|
108
115
|
it('reports flapping when live check succeeds but final status shows extension disconnected', async () => {
|
|
109
|
-
// Live check succeeds
|
|
110
116
|
mockConnect.mockResolvedValueOnce({
|
|
111
117
|
evaluate: vi.fn().mockResolvedValue(2),
|
|
112
118
|
});
|
|
113
119
|
mockClose.mockResolvedValueOnce(undefined);
|
|
114
|
-
// After live check, getDaemonHealth shows no-extension
|
|
115
120
|
mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } });
|
|
116
121
|
const report = await runBrowserDoctor({ live: true });
|
|
117
122
|
expect(report.daemonRunning).toBe(true);
|
|
@@ -122,12 +127,10 @@ describe('doctor report rendering', () => {
|
|
|
122
127
|
]));
|
|
123
128
|
});
|
|
124
129
|
it('reports daemon flapping when live check succeeds but daemon disappears afterward', async () => {
|
|
125
|
-
// Live check succeeds
|
|
126
130
|
mockConnect.mockResolvedValueOnce({
|
|
127
131
|
evaluate: vi.fn().mockResolvedValue(2),
|
|
128
132
|
});
|
|
129
133
|
mockClose.mockResolvedValueOnce(undefined);
|
|
130
|
-
// After live check, getDaemonHealth shows stopped
|
|
131
134
|
mockGetDaemonHealth.mockResolvedValueOnce({ state: 'stopped', status: null });
|
|
132
135
|
const report = await runBrowserDoctor({ live: true });
|
|
133
136
|
expect(report.daemonRunning).toBe(false);
|
|
@@ -151,14 +154,27 @@ describe('doctor report rendering', () => {
|
|
|
151
154
|
expect(timeoutSeen).toBe(8);
|
|
152
155
|
});
|
|
153
156
|
it('skips auto-start in no-live mode when daemon is already running', async () => {
|
|
154
|
-
// no-live mode but daemon already running (no-extension)
|
|
155
157
|
mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } });
|
|
156
|
-
// Final status: same
|
|
157
158
|
mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } });
|
|
158
159
|
const report = await runBrowserDoctor({ live: false });
|
|
159
|
-
// Should NOT have tried auto-start since daemon was already running
|
|
160
160
|
expect(mockConnect).not.toHaveBeenCalled();
|
|
161
161
|
expect(report.daemonRunning).toBe(true);
|
|
162
162
|
expect(report.extensionConnected).toBe(false);
|
|
163
163
|
});
|
|
164
|
+
it('reports an issue when the extension is connected but does not report a version', async () => {
|
|
165
|
+
const status = {
|
|
166
|
+
state: 'ready',
|
|
167
|
+
status: {
|
|
168
|
+
extensionConnected: true,
|
|
169
|
+
extensionVersion: undefined,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
mockGetDaemonHealth
|
|
173
|
+
.mockResolvedValueOnce(status)
|
|
174
|
+
.mockResolvedValueOnce(status);
|
|
175
|
+
const report = await runBrowserDoctor({ live: false });
|
|
176
|
+
expect(report.issues).toEqual(expect.arrayContaining([
|
|
177
|
+
expect.stringContaining('did not report a version'),
|
|
178
|
+
]));
|
|
179
|
+
});
|
|
164
180
|
});
|
|
@@ -129,6 +129,7 @@ export async function downloadArticle(data, options) {
|
|
|
129
129
|
publish_time: '-',
|
|
130
130
|
status: 'failed — no title',
|
|
131
131
|
size: '-',
|
|
132
|
+
saved: '-',
|
|
132
133
|
}];
|
|
133
134
|
}
|
|
134
135
|
if (!data.contentHtml) {
|
|
@@ -138,6 +139,7 @@ export async function downloadArticle(data, options) {
|
|
|
138
139
|
publish_time: data.publishTime || '-',
|
|
139
140
|
status: 'failed — no content',
|
|
140
141
|
size: '-',
|
|
142
|
+
saved: '-',
|
|
141
143
|
}];
|
|
142
144
|
}
|
|
143
145
|
// Convert HTML to Markdown
|
|
@@ -174,5 +176,6 @@ export async function downloadArticle(data, options) {
|
|
|
174
176
|
publish_time: data.publishTime || '-',
|
|
175
177
|
status: 'success',
|
|
176
178
|
size: formatBytes(size),
|
|
179
|
+
saved: filePath,
|
|
177
180
|
}];
|
|
178
181
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { downloadArticle } from './article-download.js';
|
|
6
|
+
const tempDirs = [];
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
for (const dir of tempDirs) {
|
|
9
|
+
try {
|
|
10
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
// Ignore cleanup errors in tests.
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
tempDirs.length = 0;
|
|
17
|
+
});
|
|
18
|
+
describe('downloadArticle', () => {
|
|
19
|
+
it('returns the saved markdown file path on success', async () => {
|
|
20
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-article-'));
|
|
21
|
+
tempDirs.push(tempDir);
|
|
22
|
+
const result = await downloadArticle({
|
|
23
|
+
title: 'Test Article',
|
|
24
|
+
author: 'Author',
|
|
25
|
+
publishTime: '2026-04-20 12:00:00',
|
|
26
|
+
sourceUrl: 'https://example.com/article',
|
|
27
|
+
contentHtml: '<p>Hello world</p>',
|
|
28
|
+
}, {
|
|
29
|
+
output: tempDir,
|
|
30
|
+
downloadImages: false,
|
|
31
|
+
});
|
|
32
|
+
expect(result).toHaveLength(1);
|
|
33
|
+
expect(result[0].status).toBe('success');
|
|
34
|
+
expect(result[0].saved).toMatch(new RegExp(`^${tempDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
|
|
35
|
+
expect(path.extname(result[0].saved)).toBe('.md');
|
|
36
|
+
expect(fs.existsSync(result[0].saved)).toBe(true);
|
|
37
|
+
expect(fs.readFileSync(result[0].saved, 'utf8')).toContain('Hello world');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
homepage: "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli"
|
|
37
37
|
tags: [dingtalk, collaboration, productivity, ai-agent]
|
|
38
38
|
install:
|
|
39
|
-
mac: "
|
|
40
|
-
linux: "
|
|
39
|
+
mac: "npm install -g dingtalk-workspace-cli"
|
|
40
|
+
linux: "npm install -g dingtalk-workspace-cli"
|
|
41
41
|
|
|
42
42
|
- name: wecom-cli
|
|
43
43
|
binary: wecom-cli
|
package/dist/src/logger.d.ts
CHANGED
|
@@ -15,9 +15,9 @@ export declare const log: {
|
|
|
15
15
|
warn(msg: string): void;
|
|
16
16
|
/** Error (always shown) */
|
|
17
17
|
error(msg: string): void;
|
|
18
|
-
/** Verbose output (shown when -v flag
|
|
18
|
+
/** Verbose output (shown when -v flag or OPENCLI_VERBOSE is set) */
|
|
19
19
|
verbose(msg: string): void;
|
|
20
|
-
/**
|
|
20
|
+
/** Alias for verbose output. */
|
|
21
21
|
debug(msg: string): void;
|
|
22
22
|
/** Step-style debug (for pipeline steps, etc.) */
|
|
23
23
|
step(stepNum: number, total: number, op: string, preview?: string): void;
|
package/dist/src/logger.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { styleText } from 'node:util';
|
|
8
8
|
function isVerbose() {
|
|
9
|
-
return !!process.env.OPENCLI_VERBOSE
|
|
9
|
+
return !!process.env.OPENCLI_VERBOSE;
|
|
10
10
|
}
|
|
11
11
|
export const log = {
|
|
12
12
|
/** Informational message (always shown) */
|
|
@@ -29,13 +29,13 @@ export const log = {
|
|
|
29
29
|
error(msg) {
|
|
30
30
|
process.stderr.write(`${styleText('red', '✖')} ${msg}\n`);
|
|
31
31
|
},
|
|
32
|
-
/** Verbose output (shown when -v flag
|
|
32
|
+
/** Verbose output (shown when -v flag or OPENCLI_VERBOSE is set) */
|
|
33
33
|
verbose(msg) {
|
|
34
34
|
if (isVerbose()) {
|
|
35
35
|
process.stderr.write(`${styleText('dim', '[verbose]')} ${msg}\n`);
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
|
-
/**
|
|
38
|
+
/** Alias for verbose output. */
|
|
39
39
|
debug(msg) {
|
|
40
40
|
this.verbose(msg);
|
|
41
41
|
},
|
package/dist/src/output.js
CHANGED
|
@@ -17,12 +17,8 @@ function resolveColumns(rows, opts) {
|
|
|
17
17
|
export function render(data, opts = {}) {
|
|
18
18
|
let fmt = opts.fmt ?? 'table';
|
|
19
19
|
// Non-TTY auto-downgrade only when format was NOT explicitly passed by user.
|
|
20
|
-
// Priority: explicit -f (any value) > OUTPUT env var > TTY auto-detect > table
|
|
21
20
|
if (!opts.fmtExplicit) {
|
|
22
|
-
|
|
23
|
-
if (envFmt)
|
|
24
|
-
fmt = envFmt;
|
|
25
|
-
else if (fmt === 'table' && !process.stdout.isTTY)
|
|
21
|
+
if (fmt === 'table' && !process.stdout.isTTY)
|
|
26
22
|
fmt = 'yaml';
|
|
27
23
|
}
|
|
28
24
|
if (data === null || data === undefined) {
|
package/dist/src/output.test.js
CHANGED
|
@@ -2,17 +2,12 @@ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
|
2
2
|
import { render } from './output.js';
|
|
3
3
|
describe('output TTY detection', () => {
|
|
4
4
|
const originalIsTTY = process.stdout.isTTY;
|
|
5
|
-
const originalEnv = process.env.OUTPUT;
|
|
6
5
|
let logSpy;
|
|
7
6
|
beforeEach(() => {
|
|
8
7
|
logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
9
8
|
});
|
|
10
9
|
afterEach(() => {
|
|
11
10
|
Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, writable: true });
|
|
12
|
-
if (originalEnv === undefined)
|
|
13
|
-
delete process.env.OUTPUT;
|
|
14
|
-
else
|
|
15
|
-
process.env.OUTPUT = originalEnv;
|
|
16
11
|
logSpy.mockRestore();
|
|
17
12
|
});
|
|
18
13
|
it('outputs YAML in non-TTY when format is default table', () => {
|
|
@@ -35,22 +30,6 @@ describe('output TTY detection', () => {
|
|
|
35
30
|
const out = logSpy.mock.calls.map((c) => c[0]).join('\n');
|
|
36
31
|
expect(JSON.parse(out)).toEqual([{ name: 'alice' }]);
|
|
37
32
|
});
|
|
38
|
-
it('OUTPUT env var overrides default table in non-TTY', () => {
|
|
39
|
-
Object.defineProperty(process.stdout, 'isTTY', { value: false, writable: true });
|
|
40
|
-
process.env.OUTPUT = 'json';
|
|
41
|
-
render([{ name: 'alice' }], { fmt: 'table' });
|
|
42
|
-
const out = logSpy.mock.calls.map((c) => c[0]).join('\n');
|
|
43
|
-
expect(JSON.parse(out)).toEqual([{ name: 'alice' }]);
|
|
44
|
-
});
|
|
45
|
-
it('explicit -f flag takes precedence over OUTPUT env var', () => {
|
|
46
|
-
Object.defineProperty(process.stdout, 'isTTY', { value: false, writable: true });
|
|
47
|
-
process.env.OUTPUT = 'json';
|
|
48
|
-
render([{ name: 'alice' }], { fmt: 'csv', fmtExplicit: true });
|
|
49
|
-
const out = logSpy.mock.calls.map((c) => c[0]).join('\n');
|
|
50
|
-
expect(out).toContain('name');
|
|
51
|
-
expect(out).toContain('alice');
|
|
52
|
-
expect(out).not.toContain('"name"'); // not JSON
|
|
53
|
-
});
|
|
54
33
|
it('explicit -f table overrides non-TTY auto-downgrade', () => {
|
|
55
34
|
Object.defineProperty(process.stdout, 'isTTY', { value: false, writable: true });
|
|
56
35
|
render([{ name: 'alice' }], { fmt: 'table', fmtExplicit: true, columns: ['name'] });
|
|
@@ -40,7 +40,7 @@ export async function stepMap(_page, params, data, args) {
|
|
|
40
40
|
for (const [key, template] of Object.entries(templateParams)) {
|
|
41
41
|
if (key === 'select')
|
|
42
42
|
continue;
|
|
43
|
-
row[key] = render(template, { args, data: source, item, index: i });
|
|
43
|
+
row[key] = render(template, { args, data: source, root: data, item, index: i });
|
|
44
44
|
}
|
|
45
45
|
result.push(row);
|
|
46
46
|
}
|
|
@@ -26,6 +26,7 @@ export function evalExpr(expr, ctx) {
|
|
|
26
26
|
const args = ctx.args ?? {};
|
|
27
27
|
const item = ctx.item ?? {};
|
|
28
28
|
const data = ctx.data;
|
|
29
|
+
const root = ctx.root;
|
|
29
30
|
const index = ctx.index ?? 0;
|
|
30
31
|
// ── Pipe filters: expr | filter1(arg) | filter2 ──
|
|
31
32
|
// Split on single | (not ||) so "item.a || item.b | upper" works correctly.
|
|
@@ -45,12 +46,12 @@ export function evalExpr(expr, ctx) {
|
|
|
45
46
|
if (/^\d+(\.\d+)?$/.test(expr))
|
|
46
47
|
return Number(expr);
|
|
47
48
|
// Try resolving as a simple dotted path (item.foo.bar, args.limit, index)
|
|
48
|
-
const resolved = resolvePath(expr, { args, item, data, index });
|
|
49
|
+
const resolved = resolvePath(expr, { args, item, data, root, index });
|
|
49
50
|
if (resolved !== null && resolved !== undefined)
|
|
50
51
|
return resolved;
|
|
51
52
|
// Fallback: evaluate as JS in a sandboxed VM.
|
|
52
53
|
// Handles ||, ??, arithmetic, ternary, method calls, etc. natively.
|
|
53
|
-
return evalJsExpr(expr, { args, item, data, index });
|
|
54
|
+
return evalJsExpr(expr, { args, item, data, root, index });
|
|
54
55
|
}
|
|
55
56
|
/**
|
|
56
57
|
* Apply a named filter to a value.
|
|
@@ -143,6 +144,7 @@ export function resolvePath(pathStr, ctx) {
|
|
|
143
144
|
const args = ctx.args ?? {};
|
|
144
145
|
const item = ctx.item ?? {};
|
|
145
146
|
const data = ctx.data;
|
|
147
|
+
const root = ctx.root;
|
|
146
148
|
const index = ctx.index ?? 0;
|
|
147
149
|
const parts = pathStr.split('.');
|
|
148
150
|
const rootName = parts[0];
|
|
@@ -160,6 +162,10 @@ export function resolvePath(pathStr, ctx) {
|
|
|
160
162
|
obj = data;
|
|
161
163
|
rest = parts.slice(1);
|
|
162
164
|
}
|
|
165
|
+
else if (rootName === 'root') {
|
|
166
|
+
obj = root;
|
|
167
|
+
rest = parts.slice(1);
|
|
168
|
+
}
|
|
163
169
|
else if (rootName === 'index')
|
|
164
170
|
return index;
|
|
165
171
|
else {
|
|
@@ -261,6 +267,7 @@ function getReusableContext() {
|
|
|
261
267
|
args: {},
|
|
262
268
|
item: {},
|
|
263
269
|
data: null,
|
|
270
|
+
root: null,
|
|
264
271
|
index: 0,
|
|
265
272
|
encodeURIComponent,
|
|
266
273
|
decodeURIComponent,
|
|
@@ -279,7 +286,7 @@ function getReusableContext() {
|
|
|
279
286
|
}
|
|
280
287
|
/** Properties that are part of the sandbox's initial shape and safe to keep. */
|
|
281
288
|
const SANDBOX_WHITELIST = new Set([
|
|
282
|
-
'args', 'item', 'data', 'index',
|
|
289
|
+
'args', 'item', 'data', 'root', 'index',
|
|
283
290
|
'encodeURIComponent', 'decodeURIComponent',
|
|
284
291
|
'JSON', 'Math', 'Number', 'String', 'Boolean', 'Array', 'Date',
|
|
285
292
|
]);
|
|
@@ -304,6 +311,7 @@ function evalJsExpr(expr, ctx) {
|
|
|
304
311
|
sandbox.args = sanitizeContext(ctx.args ?? {});
|
|
305
312
|
sandbox.item = sanitizeContext(ctx.item ?? {});
|
|
306
313
|
sandbox.data = sanitizeContext(ctx.data);
|
|
314
|
+
sandbox.root = sanitizeContext(ctx.root);
|
|
307
315
|
sandbox.index = ctx.index ?? 0;
|
|
308
316
|
return script.runInContext(context, { timeout: 50 });
|
|
309
317
|
}
|
|
@@ -22,6 +22,9 @@ describe('resolvePath', () => {
|
|
|
22
22
|
it('resolves data path', () => {
|
|
23
23
|
expect(resolvePath('data.items', { data: { items: [1, 2, 3] } })).toEqual([1, 2, 3]);
|
|
24
24
|
});
|
|
25
|
+
it('resolves root path', () => {
|
|
26
|
+
expect(resolvePath('root.items', { root: { items: [1, 2, 3] } })).toEqual([1, 2, 3]);
|
|
27
|
+
});
|
|
25
28
|
it('returns null for missing path', () => {
|
|
26
29
|
expect(resolvePath('args.missing', { args: {} })).toBeUndefined();
|
|
27
30
|
});
|
|
@@ -60,6 +60,20 @@ describe('stepMap', () => {
|
|
|
60
60
|
{ title: 'Two', rank: 2 },
|
|
61
61
|
]);
|
|
62
62
|
});
|
|
63
|
+
it('keeps data bound to the selected source and exposes root separately', async () => {
|
|
64
|
+
const result = await stepMap(null, {
|
|
65
|
+
select: 'bids',
|
|
66
|
+
bid_price: '${{ data[index][0] }}',
|
|
67
|
+
ask_price: '${{ root.asks[index][0] }}',
|
|
68
|
+
}, {
|
|
69
|
+
bids: [['100', '2'], ['99', '3']],
|
|
70
|
+
asks: [['101', '1'], ['102', '4']],
|
|
71
|
+
}, {});
|
|
72
|
+
expect(result).toEqual([
|
|
73
|
+
{ bid_price: '100', ask_price: '101' },
|
|
74
|
+
{ bid_price: '99', ask_price: '102' },
|
|
75
|
+
]);
|
|
76
|
+
});
|
|
63
77
|
});
|
|
64
78
|
describe('stepFilter', () => {
|
|
65
79
|
it('filters by expression', async () => {
|
package/dist/src/plugin.d.ts
CHANGED
|
@@ -60,13 +60,6 @@ declare function resolveStoredPluginSource(lockEntry: LockEntry | undefined, plu
|
|
|
60
60
|
*/
|
|
61
61
|
type MoveDirFsOps = Pick<typeof fs, 'renameSync' | 'cpSync' | 'rmSync'>;
|
|
62
62
|
declare function moveDir(src: string, dest: string, fsOps?: MoveDirFsOps): void;
|
|
63
|
-
type PromoteDirFsOps = MoveDirFsOps & Pick<typeof fs, 'existsSync' | 'mkdirSync'>;
|
|
64
|
-
/**
|
|
65
|
-
* Promote a prepared staging directory into its final location.
|
|
66
|
-
* The final path is only exposed after the directory has been fully prepared.
|
|
67
|
-
*/
|
|
68
|
-
declare function promoteDir(stagingDir: string, dest: string, fsOps?: PromoteDirFsOps): void;
|
|
69
|
-
declare function replaceDir(stagingDir: string, dest: string, fsOps?: PromoteDirFsOps): void;
|
|
70
63
|
export interface ValidationResult {
|
|
71
64
|
valid: boolean;
|
|
72
65
|
errors: string[];
|
|
@@ -86,7 +79,13 @@ export declare function getCommitHash(dir: string): string | undefined;
|
|
|
86
79
|
export declare function validatePluginStructure(pluginDir: string): ValidationResult;
|
|
87
80
|
declare function installDependencies(dir: string): void;
|
|
88
81
|
/**
|
|
89
|
-
* Monorepo lifecycle: install shared deps
|
|
82
|
+
* Monorepo lifecycle: install shared deps at repo root, then install and finalize each sub-plugin.
|
|
83
|
+
*
|
|
84
|
+
* The root install covers monorepos that use npm workspaces to hoist dependencies.
|
|
85
|
+
* For monorepos that do NOT use workspaces, sub-plugins may declare their own
|
|
86
|
+
* production dependencies in their package.json. We install those per sub-plugin
|
|
87
|
+
* so that runtime imports (e.g. `undici`) can be resolved from the sub-plugin
|
|
88
|
+
* directory. When the root already satisfies all deps this is a fast no-op.
|
|
90
89
|
*/
|
|
91
90
|
declare function postInstallMonorepoLifecycle(repoDir: string, pluginDirs: string[]): void;
|
|
92
91
|
/**
|
|
@@ -143,4 +142,4 @@ declare function parseSource(source: string): ParsedSource | null;
|
|
|
143
142
|
*/
|
|
144
143
|
export declare function resolveEsbuildBin(): string | null;
|
|
145
144
|
declare function resolveHostOpencliRoot(startFile?: string): string;
|
|
146
|
-
export { resolveHostOpencliRoot as _resolveHostOpencliRoot, resolveEsbuildBin as _resolveEsbuildBin, getCommitHash as _getCommitHash, installDependencies as _installDependencies, parseSource as _parseSource, postInstallMonorepoLifecycle as _postInstallMonorepoLifecycle, readLockFile as _readLockFile, readLockFileWithWriter as _readLockFileWithWriter, updateAllPlugins as _updateAllPlugins, validatePluginStructure as _validatePluginStructure, writeLockFile as _writeLockFile, writeLockFileWithFs as _writeLockFileWithFs, isSymlinkSync as _isSymlinkSync, getMonoreposDir as _getMonoreposDir, installLocalPlugin as _installLocalPlugin, isLocalPluginSource as _isLocalPluginSource, moveDir as _moveDir,
|
|
145
|
+
export { resolveHostOpencliRoot as _resolveHostOpencliRoot, resolveEsbuildBin as _resolveEsbuildBin, getCommitHash as _getCommitHash, installDependencies as _installDependencies, parseSource as _parseSource, postInstallMonorepoLifecycle as _postInstallMonorepoLifecycle, readLockFile as _readLockFile, readLockFileWithWriter as _readLockFileWithWriter, updateAllPlugins as _updateAllPlugins, validatePluginStructure as _validatePluginStructure, writeLockFile as _writeLockFile, writeLockFileWithFs as _writeLockFileWithFs, isSymlinkSync as _isSymlinkSync, getMonoreposDir as _getMonoreposDir, installLocalPlugin as _installLocalPlugin, isLocalPluginSource as _isLocalPluginSource, moveDir as _moveDir, resolvePluginSource as _resolvePluginSource, resolveStoredPluginSource as _resolveStoredPluginSource, toStoredPluginSource as _toStoredPluginSource, toLocalPluginSource as _toLocalPluginSource, };
|
package/dist/src/plugin.js
CHANGED
|
@@ -159,32 +159,6 @@ function createSiblingTempPath(dest, kind) {
|
|
|
159
159
|
const suffix = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
160
160
|
return path.join(path.dirname(dest), `.${path.basename(dest)}.${kind}-${suffix}`);
|
|
161
161
|
}
|
|
162
|
-
/**
|
|
163
|
-
* Promote a prepared staging directory into its final location.
|
|
164
|
-
* The final path is only exposed after the directory has been fully prepared.
|
|
165
|
-
*/
|
|
166
|
-
function promoteDir(stagingDir, dest, fsOps = fs) {
|
|
167
|
-
if (fsOps.existsSync(dest)) {
|
|
168
|
-
throw new PluginError(`Destination already exists: ${dest}`);
|
|
169
|
-
}
|
|
170
|
-
fsOps.mkdirSync(path.dirname(dest), { recursive: true });
|
|
171
|
-
const tempDest = createSiblingTempPath(dest, 'tmp');
|
|
172
|
-
try {
|
|
173
|
-
moveDir(stagingDir, tempDest, fsOps);
|
|
174
|
-
fsOps.renameSync(tempDest, dest);
|
|
175
|
-
}
|
|
176
|
-
catch (err) {
|
|
177
|
-
try {
|
|
178
|
-
fsOps.rmSync(tempDest, { recursive: true, force: true });
|
|
179
|
-
}
|
|
180
|
-
catch { }
|
|
181
|
-
throw err;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
function replaceDir(stagingDir, dest, fsOps = fs) {
|
|
185
|
-
const replacement = beginReplaceDir(stagingDir, dest, fsOps);
|
|
186
|
-
replacement.finalize();
|
|
187
|
-
}
|
|
188
162
|
function cloneRepoToTemp(cloneUrl) {
|
|
189
163
|
const tmpCloneDir = path.join(os.tmpdir(), `opencli-clone-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
190
164
|
try {
|
|
@@ -492,6 +466,19 @@ export function validatePluginStructure(pluginDir) {
|
|
|
492
466
|
}
|
|
493
467
|
return { valid: errors.length === 0, errors };
|
|
494
468
|
}
|
|
469
|
+
/** Check whether a directory has its own production dependencies in package.json. */
|
|
470
|
+
function hasOwnDependencies(dir) {
|
|
471
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
472
|
+
if (!fs.existsSync(pkgPath))
|
|
473
|
+
return false;
|
|
474
|
+
try {
|
|
475
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
476
|
+
return pkg.dependencies != null && Object.keys(pkg.dependencies).length > 0;
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
495
482
|
function installDependencies(dir) {
|
|
496
483
|
const pkgJsonPath = path.join(dir, 'package.json');
|
|
497
484
|
if (!fs.existsSync(pkgJsonPath))
|
|
@@ -523,11 +510,20 @@ function postInstallLifecycle(pluginDir) {
|
|
|
523
510
|
finalizePluginRuntime(pluginDir);
|
|
524
511
|
}
|
|
525
512
|
/**
|
|
526
|
-
* Monorepo lifecycle: install shared deps
|
|
513
|
+
* Monorepo lifecycle: install shared deps at repo root, then install and finalize each sub-plugin.
|
|
514
|
+
*
|
|
515
|
+
* The root install covers monorepos that use npm workspaces to hoist dependencies.
|
|
516
|
+
* For monorepos that do NOT use workspaces, sub-plugins may declare their own
|
|
517
|
+
* production dependencies in their package.json. We install those per sub-plugin
|
|
518
|
+
* so that runtime imports (e.g. `undici`) can be resolved from the sub-plugin
|
|
519
|
+
* directory. When the root already satisfies all deps this is a fast no-op.
|
|
527
520
|
*/
|
|
528
521
|
function postInstallMonorepoLifecycle(repoDir, pluginDirs) {
|
|
529
522
|
installDependencies(repoDir);
|
|
530
523
|
for (const pluginDir of pluginDirs) {
|
|
524
|
+
if (pluginDir !== repoDir && hasOwnDependencies(pluginDir)) {
|
|
525
|
+
installDependencies(pluginDir);
|
|
526
|
+
}
|
|
531
527
|
finalizePluginRuntime(pluginDir);
|
|
532
528
|
}
|
|
533
529
|
}
|
|
@@ -1246,4 +1242,4 @@ function transpilePluginTs(pluginDir) {
|
|
|
1246
1242
|
log.warn(`TS transpilation setup failed: ${getErrorMessage(err)}`);
|
|
1247
1243
|
}
|
|
1248
1244
|
}
|
|
1249
|
-
export { resolveHostOpencliRoot as _resolveHostOpencliRoot, resolveEsbuildBin as _resolveEsbuildBin, getCommitHash as _getCommitHash, installDependencies as _installDependencies, parseSource as _parseSource, postInstallMonorepoLifecycle as _postInstallMonorepoLifecycle, readLockFile as _readLockFile, readLockFileWithWriter as _readLockFileWithWriter, updateAllPlugins as _updateAllPlugins, validatePluginStructure as _validatePluginStructure, writeLockFile as _writeLockFile, writeLockFileWithFs as _writeLockFileWithFs, isSymlinkSync as _isSymlinkSync, getMonoreposDir as _getMonoreposDir, installLocalPlugin as _installLocalPlugin, isLocalPluginSource as _isLocalPluginSource, moveDir as _moveDir,
|
|
1245
|
+
export { resolveHostOpencliRoot as _resolveHostOpencliRoot, resolveEsbuildBin as _resolveEsbuildBin, getCommitHash as _getCommitHash, installDependencies as _installDependencies, parseSource as _parseSource, postInstallMonorepoLifecycle as _postInstallMonorepoLifecycle, readLockFile as _readLockFile, readLockFileWithWriter as _readLockFileWithWriter, updateAllPlugins as _updateAllPlugins, validatePluginStructure as _validatePluginStructure, writeLockFile as _writeLockFile, writeLockFileWithFs as _writeLockFileWithFs, isSymlinkSync as _isSymlinkSync, getMonoreposDir as _getMonoreposDir, installLocalPlugin as _installLocalPlugin, isLocalPluginSource as _isLocalPluginSource, moveDir as _moveDir, resolvePluginSource as _resolvePluginSource, resolveStoredPluginSource as _resolveStoredPluginSource, toStoredPluginSource as _toStoredPluginSource, toLocalPluginSource as _toLocalPluginSource, };
|
package/dist/src/plugin.test.js
CHANGED
|
@@ -12,7 +12,7 @@ const { mockExecFileSync, mockExecSync } = vi.hoisted(() => ({
|
|
|
12
12
|
mockExecFileSync: vi.fn(),
|
|
13
13
|
mockExecSync: vi.fn(),
|
|
14
14
|
}));
|
|
15
|
-
const { _getCommitHash, _installDependencies, _postInstallMonorepoLifecycle,
|
|
15
|
+
const { _getCommitHash, _installDependencies, _postInstallMonorepoLifecycle, installPlugin, listPlugins, _readLockFile, _readLockFileWithWriter, _resolveEsbuildBin, _resolveHostOpencliRoot, uninstallPlugin, updatePlugin, _parseSource, _updateAllPlugins, _validatePluginStructure, _writeLockFile, _writeLockFileWithFs, _isSymlinkSync, _getMonoreposDir, getLockFilePath, _installLocalPlugin, _isLocalPluginSource, _moveDir, _resolvePluginSource, _resolveStoredPluginSource, _toStoredPluginSource, _toLocalPluginSource, } = pluginModule;
|
|
16
16
|
describe('parseSource', () => {
|
|
17
17
|
it('parses github:user/repo format', () => {
|
|
18
18
|
const result = _parseSource('github:ByteYue/opencli-plugin-github-trending');
|
|
@@ -545,13 +545,27 @@ describe('postInstallMonorepoLifecycle', () => {
|
|
|
545
545
|
afterEach(() => {
|
|
546
546
|
fs.rmSync(repoDir, { recursive: true, force: true });
|
|
547
547
|
});
|
|
548
|
-
it('installs dependencies
|
|
548
|
+
it('installs dependencies at the monorepo root and skips sub-plugins without own dependencies', () => {
|
|
549
549
|
_postInstallMonorepoLifecycle(repoDir, [subDir]);
|
|
550
550
|
const npmCalls = mockExecFileSync.mock.calls.filter(([cmd, args]) => cmd === 'npm' && Array.isArray(args) && args[0] === 'install');
|
|
551
551
|
expect(npmCalls).toHaveLength(1);
|
|
552
552
|
expect(npmCalls[0][2]).toMatchObject({ cwd: repoDir });
|
|
553
553
|
expect(npmCalls.some(([, , opts]) => opts?.cwd === subDir)).toBe(false);
|
|
554
554
|
});
|
|
555
|
+
it('also installs dependencies in sub-plugins that declare their own production dependencies', () => {
|
|
556
|
+
// Give the sub-plugin its own production dependencies
|
|
557
|
+
fs.writeFileSync(path.join(subDir, 'package.json'), JSON.stringify({
|
|
558
|
+
name: 'opencli-plugin-alpha',
|
|
559
|
+
version: '1.0.0',
|
|
560
|
+
type: 'module',
|
|
561
|
+
dependencies: { undici: '^8.0.0' },
|
|
562
|
+
}));
|
|
563
|
+
_postInstallMonorepoLifecycle(repoDir, [subDir]);
|
|
564
|
+
const npmCalls = mockExecFileSync.mock.calls.filter(([cmd, args]) => cmd === 'npm' && Array.isArray(args) && args[0] === 'install');
|
|
565
|
+
expect(npmCalls).toHaveLength(2);
|
|
566
|
+
expect(npmCalls[0][2]).toMatchObject({ cwd: repoDir });
|
|
567
|
+
expect(npmCalls[1][2]).toMatchObject({ cwd: subDir });
|
|
568
|
+
});
|
|
555
569
|
});
|
|
556
570
|
describe('updateAllPlugins', () => {
|
|
557
571
|
const testDirA = path.join(PLUGINS_DIR, 'plugin-a');
|
|
@@ -910,64 +924,6 @@ describe('moveDir', () => {
|
|
|
910
924
|
expect(rmSync).toHaveBeenCalledWith(dest, { recursive: true, force: true });
|
|
911
925
|
});
|
|
912
926
|
});
|
|
913
|
-
describe('promoteDir', () => {
|
|
914
|
-
it('cleans up temporary publish dir when final rename fails', () => {
|
|
915
|
-
const staging = path.join(os.tmpdir(), 'opencli-promote-stage');
|
|
916
|
-
const dest = path.join(os.tmpdir(), 'opencli-promote-dest');
|
|
917
|
-
const publishErr = new Error('publish failed');
|
|
918
|
-
const existsSync = vi.fn(() => false);
|
|
919
|
-
const mkdirSync = vi.fn(() => undefined);
|
|
920
|
-
const cpSync = vi.fn(() => undefined);
|
|
921
|
-
const rmSync = vi.fn(() => undefined);
|
|
922
|
-
const renameSync = vi.fn((src, _target) => {
|
|
923
|
-
if (String(src) === staging)
|
|
924
|
-
return;
|
|
925
|
-
throw publishErr;
|
|
926
|
-
});
|
|
927
|
-
expect(() => _promoteDir(staging, dest, { existsSync, mkdirSync, renameSync, cpSync, rmSync })).toThrow(publishErr);
|
|
928
|
-
const tempDest = renameSync.mock.calls[0][1];
|
|
929
|
-
expect(renameSync).toHaveBeenNthCalledWith(1, staging, tempDest);
|
|
930
|
-
expect(renameSync).toHaveBeenNthCalledWith(2, tempDest, dest);
|
|
931
|
-
expect(rmSync).toHaveBeenCalledWith(tempDest, { recursive: true, force: true });
|
|
932
|
-
});
|
|
933
|
-
});
|
|
934
|
-
describe('replaceDir', () => {
|
|
935
|
-
it('rolls back the original destination when swap fails', () => {
|
|
936
|
-
const staging = path.join(os.tmpdir(), 'opencli-replace-stage');
|
|
937
|
-
const dest = path.join(os.tmpdir(), 'opencli-replace-dest');
|
|
938
|
-
const publishErr = new Error('swap failed');
|
|
939
|
-
const existingPaths = new Set([dest]);
|
|
940
|
-
const existsSync = vi.fn((p) => existingPaths.has(String(p)));
|
|
941
|
-
const mkdirSync = vi.fn(() => undefined);
|
|
942
|
-
const cpSync = vi.fn(() => undefined);
|
|
943
|
-
const rmSync = vi.fn(() => undefined);
|
|
944
|
-
const renameSync = vi.fn((src, target) => {
|
|
945
|
-
if (String(src) === staging) {
|
|
946
|
-
existingPaths.add(String(target));
|
|
947
|
-
return;
|
|
948
|
-
}
|
|
949
|
-
if (String(src) === dest) {
|
|
950
|
-
existingPaths.delete(dest);
|
|
951
|
-
existingPaths.add(String(target));
|
|
952
|
-
return;
|
|
953
|
-
}
|
|
954
|
-
if (String(target) === dest)
|
|
955
|
-
throw publishErr;
|
|
956
|
-
if (existingPaths.has(String(src))) {
|
|
957
|
-
existingPaths.delete(String(src));
|
|
958
|
-
existingPaths.add(String(target));
|
|
959
|
-
}
|
|
960
|
-
});
|
|
961
|
-
expect(() => _replaceDir(staging, dest, { existsSync, mkdirSync, renameSync, cpSync, rmSync })).toThrow(publishErr);
|
|
962
|
-
const tempDest = renameSync.mock.calls[0][1];
|
|
963
|
-
const backupDest = renameSync.mock.calls[1][1];
|
|
964
|
-
expect(renameSync).toHaveBeenNthCalledWith(1, staging, tempDest);
|
|
965
|
-
expect(renameSync).toHaveBeenNthCalledWith(2, dest, backupDest);
|
|
966
|
-
expect(renameSync).toHaveBeenNthCalledWith(3, tempDest, dest);
|
|
967
|
-
expect(renameSync).toHaveBeenNthCalledWith(4, backupDest, dest);
|
|
968
|
-
expect(rmSync).toHaveBeenCalledWith(tempDest, { recursive: true, force: true });
|
|
969
|
-
});
|
|
970
|
-
});
|
|
971
927
|
describe('installPlugin transactional staging', () => {
|
|
972
928
|
const standaloneSource = 'github:user/opencli-plugin-__test-transactional-standalone__';
|
|
973
929
|
const standaloneName = '__test-transactional-standalone__';
|
package/dist/src/registry.d.ts
CHANGED
package/dist/src/registry.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
export var Strategy;
|
|
5
5
|
(function (Strategy) {
|
|
6
6
|
Strategy["PUBLIC"] = "public";
|
|
7
|
+
Strategy["LOCAL"] = "local";
|
|
7
8
|
Strategy["COOKIE"] = "cookie";
|
|
8
9
|
Strategy["HEADER"] = "header";
|
|
9
10
|
Strategy["INTERCEPT"] = "intercept";
|
|
@@ -58,13 +59,13 @@ export function strategyLabel(cmd) {
|
|
|
58
59
|
*/
|
|
59
60
|
function normalizeCommand(cmd) {
|
|
60
61
|
const strategy = cmd.strategy ?? (cmd.browser === false ? Strategy.PUBLIC : Strategy.COOKIE);
|
|
61
|
-
const browser = cmd.browser ?? (strategy !== Strategy.PUBLIC);
|
|
62
|
+
const browser = cmd.browser ?? (strategy !== Strategy.PUBLIC && strategy !== Strategy.LOCAL);
|
|
62
63
|
let navigateBefore = cmd.navigateBefore;
|
|
63
64
|
if (navigateBefore === undefined) {
|
|
64
65
|
if ((strategy === Strategy.COOKIE || strategy === Strategy.HEADER) && cmd.domain) {
|
|
65
66
|
navigateBefore = `https://${cmd.domain}`;
|
|
66
67
|
}
|
|
67
|
-
else if (strategy !== Strategy.PUBLIC) {
|
|
68
|
+
else if (strategy !== Strategy.PUBLIC && strategy !== Strategy.LOCAL) {
|
|
68
69
|
// Non-PUBLIC without domain: needs authenticated browser context
|
|
69
70
|
// but no specific pre-navigation URL. `true` signals this to
|
|
70
71
|
// shouldUseBrowserSession without triggering resolvePreNav.
|