@jackwener/opencli 1.8.0 → 1.8.1
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 +8 -49
- package/README.zh-CN.md +8 -52
- package/cli-manifest.json +1796 -191
- package/clis/_atlassian/shared.js +577 -0
- package/clis/_atlassian/shared.test.js +170 -0
- package/clis/bilibili/comment.js +125 -0
- package/clis/bilibili/comment.test.js +153 -0
- package/clis/bilibili/comments.js +116 -21
- package/clis/bilibili/comments.test.js +77 -18
- package/clis/bilibili/subtitle.js +76 -31
- package/clis/bilibili/subtitle.test.js +156 -9
- package/clis/bilibili/utils.js +63 -5
- package/clis/bilibili/utils.test.js +45 -1
- package/clis/chess/analyze.js +35 -0
- package/clis/chess/analyze.test.js +79 -0
- package/clis/chess/game.js +114 -0
- package/clis/chess/game.test.js +178 -0
- package/clis/chess/games.js +67 -0
- package/clis/chess/games.test.js +164 -0
- package/clis/chess/stats.js +32 -0
- package/clis/chess/stats.test.js +79 -0
- package/clis/chess/utils.js +170 -0
- package/clis/chess/utils.test.js +230 -0
- package/clis/confluence/commands.test.js +195 -0
- package/clis/confluence/create.js +39 -0
- package/clis/confluence/page.js +23 -0
- package/clis/confluence/search.js +34 -0
- package/clis/confluence/shared.js +173 -0
- package/clis/confluence/update.js +38 -0
- package/clis/douyin/hashtag.js +84 -23
- package/clis/douyin/hashtag.test.js +113 -0
- package/clis/geogebra/add-circle.js +46 -0
- package/clis/geogebra/add-line.js +35 -0
- package/clis/geogebra/add-point.js +27 -0
- package/clis/geogebra/add-polygon.js +25 -0
- package/clis/geogebra/eval.js +35 -0
- package/clis/geogebra/geogebra.test.js +175 -0
- package/clis/geogebra/hexagon.js +62 -0
- package/clis/geogebra/info.js +72 -0
- package/clis/geogebra/list.js +35 -0
- package/clis/geogebra/triangle.js +60 -0
- package/clis/geogebra/utils.js +271 -0
- package/clis/jira/attachments.js +28 -0
- package/clis/jira/commands.test.js +287 -0
- package/clis/jira/comments.js +28 -0
- package/clis/jira/issue.js +28 -0
- package/clis/jira/links.js +28 -0
- package/clis/jira/search.js +47 -0
- package/clis/jira/shared.js +256 -0
- package/clis/linkedin/job-detail.js +167 -0
- package/clis/linkedin/job-detail.test.js +38 -0
- package/clis/linkedin/jobs-preferences.js +113 -0
- package/clis/linkedin/jobs-preferences.test.js +43 -0
- package/clis/linkedin/post-analytics.js +74 -0
- package/clis/linkedin/post-analytics.test.js +40 -0
- package/clis/linkedin/posts-core.js +241 -0
- package/clis/linkedin/posts.js +22 -0
- package/clis/linkedin/posts.test.js +40 -0
- package/clis/linkedin/profile-analytics.js +104 -0
- package/clis/linkedin/profile-analytics.test.js +67 -0
- package/clis/linkedin/profile-experience.js +671 -0
- package/clis/linkedin/profile-experience.test.js +152 -0
- package/clis/linkedin/profile-projects.js +311 -0
- package/clis/linkedin/profile-projects.test.js +111 -0
- package/clis/linkedin/profile-read.js +148 -0
- package/clis/linkedin/profile-read.test.js +77 -0
- package/clis/linkedin/services-read.js +213 -0
- package/clis/linkedin/services-read.test.js +105 -0
- package/clis/linkedin/shared.js +124 -0
- package/clis/linkedin/timeline.js +14 -7
- package/clis/notebooklm/add-source.js +269 -0
- package/clis/notebooklm/add-source.test.js +97 -0
- package/clis/notebooklm/create.js +76 -0
- package/clis/notebooklm/create.test.js +58 -0
- package/clis/notebooklm/generate-audio.js +91 -0
- package/clis/notebooklm/generate-audio.test.js +63 -0
- package/clis/notebooklm/generate-slides.js +106 -0
- package/clis/notebooklm/generate-slides.test.js +75 -0
- package/clis/notebooklm/open.test.js +10 -10
- package/clis/notebooklm/rpc.js +20 -6
- package/clis/notebooklm/rpc.test.js +27 -1
- package/clis/notebooklm/utils.js +100 -24
- package/clis/notebooklm/utils.test.js +60 -1
- package/clis/notebooklm/write-note.js +103 -0
- package/clis/notebooklm/write-note.test.js +70 -0
- package/clis/pixiv/detail.js +41 -34
- package/clis/pixiv/detail.test.js +93 -0
- package/clis/pixiv/user.js +36 -31
- package/clis/pixiv/user.test.js +100 -0
- package/clis/pixiv/utils.js +56 -7
- package/clis/suno/generate.js +5 -0
- package/clis/suno/generate.test.js +9 -0
- package/clis/suno/status.js +3 -2
- package/clis/suno/utils.js +33 -24
- package/clis/suno/utils.test.js +106 -0
- package/clis/twitter/followers.js +6 -2
- package/clis/twitter/followers.test.js +19 -1
- package/clis/twitter/following.js +14 -5
- package/clis/twitter/following.test.js +29 -0
- package/clis/twitter/likes.js +12 -4
- package/clis/twitter/likes.test.js +26 -1
- package/clis/twitter/list-add.js +1 -1
- package/clis/twitter/list-remove.js +1 -1
- package/clis/twitter/notifications.js +4 -4
- package/clis/twitter/post.js +62 -4
- package/clis/twitter/post.test.js +35 -3
- package/clis/twitter/profile.js +81 -28
- package/clis/twitter/profile.test.js +113 -2
- package/clis/twitter/quote.js +9 -4
- package/clis/twitter/reply.js +13 -10
- package/clis/twitter/reply.test.js +41 -0
- package/clis/twitter/search.js +1 -1
- package/clis/twitter/search.test.js +35 -0
- package/clis/twitter/shared.js +11 -0
- package/clis/twitter/shared.test.js +37 -1
- package/clis/twitter/utils.js +53 -16
- package/clis/upwork/detail.js +132 -0
- package/clis/upwork/feed.js +109 -0
- package/clis/upwork/search.js +115 -0
- package/clis/upwork/upwork.test.js +566 -0
- package/clis/upwork/utils.js +323 -0
- package/clis/weread/book-search.js +438 -0
- package/clis/weread/book-search.test.js +242 -0
- package/clis/weread/search-regression.test.js +80 -0
- package/clis/weread/search.js +17 -2
- package/clis/xiaohongshu/creator-note-detail.js +165 -28
- package/clis/xiaohongshu/creator-note-detail.test.js +186 -37
- package/clis/xiaohongshu/creator-notes.js +251 -2
- package/clis/xiaohongshu/creator-notes.test.js +79 -2
- package/clis/xiaohongshu/download.js +97 -39
- package/clis/xiaohongshu/download.test.js +201 -0
- package/clis/zhihu/answer-comments.js +2 -21
- package/clis/zhihu/answer-detail.js +2 -31
- package/clis/zhihu/collection.js +2 -14
- package/clis/zhihu/collection.test.js +4 -3
- package/clis/zhihu/question.js +1 -9
- package/clis/zhihu/question.test.js +2 -2
- package/clis/zhihu/search.js +1 -12
- package/clis/zhihu/search.test.js +2 -2
- package/clis/zhihu/text.js +29 -0
- package/clis/zhihu/text.test.js +24 -0
- package/dist/src/browser/network-cache.js +13 -1
- package/dist/src/browser/network-cache.test.js +17 -0
- package/dist/src/download/index.js +13 -1
- package/dist/src/download/index.test.js +23 -1
- package/dist/src/download/media-download.test.js +3 -1
- package/dist/src/download/progress.js +2 -2
- package/dist/src/download/progress.test.js +12 -1
- package/dist/src/output.js +11 -1
- package/dist/src/output.test.js +6 -0
- package/dist/src/registry.js +1 -0
- package/dist/src/registry.test.js +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { ensureApplet, ggbEval, normalizeCoords, normalizeLabel, requireGgbSuccess } from './utils.js';
|
|
3
|
+
|
|
4
|
+
cli({
|
|
5
|
+
site: 'geogebra',
|
|
6
|
+
name: 'add-point',
|
|
7
|
+
access: 'write',
|
|
8
|
+
description: 'Create a point with given label and coordinates',
|
|
9
|
+
domain: 'www.geogebra.org',
|
|
10
|
+
strategy: Strategy.PUBLIC,
|
|
11
|
+
browser: true,
|
|
12
|
+
navigateBefore: false,
|
|
13
|
+
example: 'opencli geogebra add-point --name A --coords 1,2',
|
|
14
|
+
args: [
|
|
15
|
+
{ name: 'name', required: true, help: 'Point label (e.g. A, B, P1)' },
|
|
16
|
+
{ name: 'coords', required: true, help: 'Coordinates as x,y (e.g. "1,2")' },
|
|
17
|
+
],
|
|
18
|
+
columns: ['name', 'x', 'y'],
|
|
19
|
+
func: async (page, kwargs) => {
|
|
20
|
+
const name = normalizeLabel(kwargs.name, 'name');
|
|
21
|
+
const [x, y] = normalizeCoords(kwargs.coords);
|
|
22
|
+
const cmd = `${name}=(${x},${y})`;
|
|
23
|
+
await ensureApplet(page);
|
|
24
|
+
const result = requireGgbSuccess(await ggbEval(page, cmd), `Failed to create point: ${cmd}`);
|
|
25
|
+
return [{ name, x, y }];
|
|
26
|
+
},
|
|
27
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { ensureApplet, ggbEval, normalizeLabelList, requireGgbSuccess } from './utils.js';
|
|
3
|
+
|
|
4
|
+
cli({
|
|
5
|
+
site: 'geogebra',
|
|
6
|
+
name: 'add-polygon',
|
|
7
|
+
access: 'write',
|
|
8
|
+
description: 'Create a polygon from a list of point labels',
|
|
9
|
+
domain: 'www.geogebra.org',
|
|
10
|
+
strategy: Strategy.PUBLIC,
|
|
11
|
+
browser: true,
|
|
12
|
+
navigateBefore: false,
|
|
13
|
+
example: 'opencli geogebra add-polygon --points A,B,C',
|
|
14
|
+
args: [
|
|
15
|
+
{ name: 'points', required: true, help: 'Comma-separated point labels (e.g. "A,B,C" or "A,B,C,D")' },
|
|
16
|
+
],
|
|
17
|
+
columns: ['label', 'vertices'],
|
|
18
|
+
func: async (page, kwargs) => {
|
|
19
|
+
const points = normalizeLabelList(kwargs.points, 'points', 3, 50);
|
|
20
|
+
const cmd = `Polygon(${points.join(',')})`;
|
|
21
|
+
await ensureApplet(page);
|
|
22
|
+
const result = requireGgbSuccess(await ggbEval(page, cmd), `Failed to create polygon: ${cmd}`);
|
|
23
|
+
return [{ label: result.label, vertices: points.join(',') }];
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { ArgumentError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { ensureApplet, ggbEval, requireGgbSuccess } from './utils.js';
|
|
4
|
+
|
|
5
|
+
cli({
|
|
6
|
+
site: 'geogebra',
|
|
7
|
+
name: 'eval',
|
|
8
|
+
access: 'write',
|
|
9
|
+
description: 'Execute one or more GeoGebra command strings (semicolon-separated)',
|
|
10
|
+
domain: 'www.geogebra.org',
|
|
11
|
+
strategy: Strategy.PUBLIC,
|
|
12
|
+
browser: true,
|
|
13
|
+
navigateBefore: false,
|
|
14
|
+
example: 'opencli geogebra eval "A=(0,0);B=(4,0);c=Circle(A,B);d=Circle(B,A);C=Intersect(c,d,1);Polygon(A,B,C)"',
|
|
15
|
+
args: [
|
|
16
|
+
{ name: 'command', positional: true, required: true, help: 'GeoGebra command string (use ; to chain multiple commands)' },
|
|
17
|
+
],
|
|
18
|
+
columns: ['command', 'result'],
|
|
19
|
+
func: async (page, kwargs) => {
|
|
20
|
+
const commands = String(kwargs.command).split(';').map(s => s.trim()).filter(Boolean);
|
|
21
|
+
if (commands.length === 0) {
|
|
22
|
+
throw new ArgumentError('command must contain at least one GeoGebra command');
|
|
23
|
+
}
|
|
24
|
+
await ensureApplet(page);
|
|
25
|
+
const results = [];
|
|
26
|
+
for (const command of commands) {
|
|
27
|
+
const result = requireGgbSuccess(await ggbEval(page, command), `Failed to execute GeoGebra command: ${command}`);
|
|
28
|
+
results.push({
|
|
29
|
+
command,
|
|
30
|
+
result: `ok (${result.label || 'no label'})`,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return results;
|
|
34
|
+
},
|
|
35
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
4
|
+
import { ensureApplet, ggbEval, ggbGetProperty, ggbListObjects, ggbWaitForObjectCount } from './utils.js';
|
|
5
|
+
import './add-circle.js';
|
|
6
|
+
import './add-line.js';
|
|
7
|
+
import './add-point.js';
|
|
8
|
+
import './add-polygon.js';
|
|
9
|
+
import './eval.js';
|
|
10
|
+
import './hexagon.js';
|
|
11
|
+
import './info.js';
|
|
12
|
+
import './list.js';
|
|
13
|
+
import './triangle.js';
|
|
14
|
+
|
|
15
|
+
function createPageMock(url = 'https://www.geogebra.org/geometry') {
|
|
16
|
+
return {
|
|
17
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
18
|
+
evaluate: vi.fn(),
|
|
19
|
+
getCurrentUrl: vi.fn().mockResolvedValue(url),
|
|
20
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
21
|
+
screenshot: vi.fn().mockResolvedValue(undefined),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('ensureApplet', () => {
|
|
26
|
+
it('skips navigation when already on the geometry page', async () => {
|
|
27
|
+
const page = createPageMock('https://www.geogebra.org/geometry');
|
|
28
|
+
page.evaluate.mockResolvedValue(true);
|
|
29
|
+
await ensureApplet(page);
|
|
30
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('unwraps Browser Bridge evaluate envelopes while checking applet readiness', async () => {
|
|
34
|
+
const page = createPageMock('https://www.geogebra.org/geometry');
|
|
35
|
+
page.evaluate.mockResolvedValue({ session: 1, data: true });
|
|
36
|
+
await ensureApplet(page);
|
|
37
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('navigates when not on the geometry page', async () => {
|
|
41
|
+
const page = createPageMock('https://example.com');
|
|
42
|
+
page.evaluate.mockResolvedValue(true);
|
|
43
|
+
await ensureApplet(page);
|
|
44
|
+
expect(page.goto).toHaveBeenCalledWith('https://www.geogebra.org/geometry');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('throws when ggbApplet never becomes available', async () => {
|
|
48
|
+
const page = createPageMock();
|
|
49
|
+
page.evaluate.mockResolvedValue(false);
|
|
50
|
+
await expect(ensureApplet(page)).rejects.toThrow(CommandExecutionError);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('ggbEval', () => {
|
|
55
|
+
it('calls evalCommandGetLabels and evalCommand', async () => {
|
|
56
|
+
const page = createPageMock();
|
|
57
|
+
page.evaluate.mockResolvedValue({ ok: true, label: 'A', beforeCount: 0, afterCount: 1, error: null });
|
|
58
|
+
const result = await ggbEval(page, 'A=(1,2)');
|
|
59
|
+
expect(result).toEqual({ ok: true, label: 'A', beforeCount: 0, afterCount: 1, error: null });
|
|
60
|
+
expect(page.evaluate).toHaveBeenCalledTimes(1);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('throws typed errors for malformed evaluate results', async () => {
|
|
64
|
+
const page = createPageMock();
|
|
65
|
+
page.evaluate.mockResolvedValue({ nope: true });
|
|
66
|
+
await expect(ggbEval(page, 'A=(1,2)')).rejects.toThrow(CommandExecutionError);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('ggbGetProperty', () => {
|
|
71
|
+
it('requests a property from the applet', async () => {
|
|
72
|
+
const page = createPageMock();
|
|
73
|
+
page.evaluate.mockResolvedValue('point');
|
|
74
|
+
const result = await ggbGetProperty(page, 'A', 'type');
|
|
75
|
+
expect(result).toBe('point');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('ggbListObjects', () => {
|
|
80
|
+
it('normalizes object rows from the applet', async () => {
|
|
81
|
+
const page = createPageMock();
|
|
82
|
+
page.evaluate.mockResolvedValue([
|
|
83
|
+
{ name: 'A', type: 'point', value: '(0, 0)', visible: true },
|
|
84
|
+
{ name: 't1', type: 'polygon', value: '', visible: true },
|
|
85
|
+
]);
|
|
86
|
+
const result = await ggbListObjects(page);
|
|
87
|
+
expect(result).toHaveLength(2);
|
|
88
|
+
expect(page.evaluate).toHaveBeenCalledTimes(1);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('unwraps Browser Bridge envelopes for object rows', async () => {
|
|
92
|
+
const page = createPageMock();
|
|
93
|
+
page.evaluate.mockResolvedValue({ session: 1, data: [{ name: 'A', type: 'point', value: '(0, 0)', visible: true }] });
|
|
94
|
+
const result = await ggbListObjects(page);
|
|
95
|
+
expect(result).toEqual([{ name: 'A', type: 'point', value: '(0, 0)', visible: true }]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('throws typed error when object names exist but property extraction fails', async () => {
|
|
99
|
+
const page = createPageMock();
|
|
100
|
+
page.evaluate.mockResolvedValue({ error: 'getObjectType failed', name: 'A' });
|
|
101
|
+
await expect(ggbListObjects(page)).rejects.toThrow(CommandExecutionError);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('ggbWaitForObjectCount', () => {
|
|
106
|
+
it('returns the detected object count', async () => {
|
|
107
|
+
const page = createPageMock();
|
|
108
|
+
page.evaluate.mockResolvedValue(4);
|
|
109
|
+
const result = await ggbWaitForObjectCount(page, 4);
|
|
110
|
+
expect(result).toBe(4);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('geogebra command typed boundaries', () => {
|
|
115
|
+
it('validates add-point args before navigating', async () => {
|
|
116
|
+
const command = getRegistry().get('geogebra/add-point');
|
|
117
|
+
const page = createPageMock('https://example.com');
|
|
118
|
+
await expect(command.func(page, { name: 'A', coords: 'bad' })).rejects.toThrow(ArgumentError);
|
|
119
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('validates triangle size before navigating instead of silently defaulting', async () => {
|
|
123
|
+
const command = getRegistry().get('geogebra/triangle');
|
|
124
|
+
const page = createPageMock('https://example.com');
|
|
125
|
+
await expect(command.func(page, { size: 'not-a-number' })).rejects.toThrow(ArgumentError);
|
|
126
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('fails eval command execution instead of returning a success row for failed commands', async () => {
|
|
130
|
+
const command = getRegistry().get('geogebra/eval');
|
|
131
|
+
const page = createPageMock('https://www.geogebra.org/geometry');
|
|
132
|
+
page.evaluate
|
|
133
|
+
.mockResolvedValueOnce(true)
|
|
134
|
+
.mockResolvedValueOnce({ ok: false, label: '', beforeCount: 0, afterCount: 0, error: 'Unknown command' });
|
|
135
|
+
await expect(command.func(page, { command: 'NotACommand(' })).rejects.toThrow(CommandExecutionError);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('creates add-line rows from validated labels', async () => {
|
|
139
|
+
const command = getRegistry().get('geogebra/add-line');
|
|
140
|
+
const page = createPageMock('https://www.geogebra.org/geometry');
|
|
141
|
+
page.evaluate
|
|
142
|
+
.mockResolvedValueOnce(true)
|
|
143
|
+
.mockResolvedValueOnce({ ok: true, label: 'f', beforeCount: 2, afterCount: 3, error: null });
|
|
144
|
+
await expect(command.func(page, { points: 'A,B', type: 'segment' })).resolves.toEqual([
|
|
145
|
+
{ label: 'f', type: 'segment', points: 'A,B' },
|
|
146
|
+
]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('does not turn object property extraction failures into empty list results', async () => {
|
|
150
|
+
const command = getRegistry().get('geogebra/list');
|
|
151
|
+
const page = createPageMock('https://www.geogebra.org/geometry');
|
|
152
|
+
page.evaluate
|
|
153
|
+
.mockResolvedValueOnce(true)
|
|
154
|
+
.mockResolvedValueOnce({ error: 'getObjectType failed', name: 'A' });
|
|
155
|
+
await expect(command.func(page, {})).rejects.toThrow(CommandExecutionError);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('does not turn malformed info existence probes into not-found results', async () => {
|
|
159
|
+
const command = getRegistry().get('geogebra/info');
|
|
160
|
+
const page = createPageMock('https://www.geogebra.org/geometry');
|
|
161
|
+
page.evaluate
|
|
162
|
+
.mockResolvedValueOnce(true)
|
|
163
|
+
.mockResolvedValueOnce({ nope: true });
|
|
164
|
+
await expect(command.func(page, { name: 'A' })).rejects.toThrow(CommandExecutionError);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('maps explicit false info existence probes to EmptyResultError', async () => {
|
|
168
|
+
const command = getRegistry().get('geogebra/info');
|
|
169
|
+
const page = createPageMock('https://www.geogebra.org/geometry');
|
|
170
|
+
page.evaluate
|
|
171
|
+
.mockResolvedValueOnce(true)
|
|
172
|
+
.mockResolvedValueOnce({ ok: true, exists: false });
|
|
173
|
+
await expect(command.func(page, { name: 'A' })).rejects.toThrow(EmptyResultError);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { CommandExecutionError } from '@jackwener/opencli/errors';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { ensureApplet, ggbEval, ggbListObjects, ggbWaitForObjectCount, normalizeNumber, requireGgbSuccess } from './utils.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Draw a regular hexagon on the GeoGebra Geometry canvas.
|
|
9
|
+
* Creates center point, vertex, and the regular polygon in one session.
|
|
10
|
+
*/
|
|
11
|
+
cli({
|
|
12
|
+
site: 'geogebra',
|
|
13
|
+
name: 'hexagon',
|
|
14
|
+
access: 'write',
|
|
15
|
+
description: 'Draw a regular hexagon centered at the origin',
|
|
16
|
+
domain: 'www.geogebra.org',
|
|
17
|
+
strategy: Strategy.PUBLIC,
|
|
18
|
+
browser: true,
|
|
19
|
+
navigateBefore: false,
|
|
20
|
+
example: 'opencli geogebra hexagon --size 3',
|
|
21
|
+
args: [
|
|
22
|
+
{ name: 'size', required: false, default: '2', help: 'Radius of the hexagon (default: 2)' },
|
|
23
|
+
],
|
|
24
|
+
columns: ['step', 'result'],
|
|
25
|
+
func: async (page, kwargs) => {
|
|
26
|
+
const size = normalizeNumber(kwargs.size, 'size', { defaultValue: 2, positive: true });
|
|
27
|
+
await ensureApplet(page);
|
|
28
|
+
const results = [];
|
|
29
|
+
|
|
30
|
+
const vertices = [
|
|
31
|
+
['V1', `(${size},0)`],
|
|
32
|
+
['V2', `(${size}*cos(pi/3),${size}*sin(pi/3))`],
|
|
33
|
+
['V3', `(${size}*cos(2*pi/3),${size}*sin(2*pi/3))`],
|
|
34
|
+
['V4', `(-${size},0)`],
|
|
35
|
+
['V5', `(${size}*cos(4*pi/3),${size}*sin(4*pi/3))`],
|
|
36
|
+
['V6', `(${size}*cos(5*pi/3),${size}*sin(5*pi/3))`],
|
|
37
|
+
];
|
|
38
|
+
for (const [name, coords] of vertices) {
|
|
39
|
+
const result = requireGgbSuccess(await ggbEval(page, `${name}=${coords}`), `Failed to create point ${name}`);
|
|
40
|
+
results.push({ step: `${name}=${coords}`, result: `ok (${result.label || name})` });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const polygon = requireGgbSuccess(await ggbEval(page, 'Hexagon=Polygon(V1,V2,V3,V4,V5,V6)'), 'Failed to create hexagon polygon');
|
|
44
|
+
results.push({ step: 'Hexagon=Polygon(V1,V2,V3,V4,V5,V6)', result: `ok (${polygon.label || 'hexagon created'})` });
|
|
45
|
+
|
|
46
|
+
const objectCount = await ggbWaitForObjectCount(page, 7);
|
|
47
|
+
const objects = await ggbListObjects(page);
|
|
48
|
+
const screenshotPath = path.join(os.tmpdir(), 'opencli-geogebra-hexagon.png');
|
|
49
|
+
try {
|
|
50
|
+
await page.screenshot({ path: screenshotPath });
|
|
51
|
+
} catch (err) {
|
|
52
|
+
throw new CommandExecutionError(`Failed to capture GeoGebra screenshot: ${err?.message || err}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (Array.isArray(objects) && objects.length > 0) {
|
|
56
|
+
results.push({ step: `canvas has ${objectCount} objects`, result: objects.map(o => `${o.name}(${o.type})`).join(', ') });
|
|
57
|
+
}
|
|
58
|
+
results.push({ step: 'screenshot', result: screenshotPath });
|
|
59
|
+
|
|
60
|
+
return results;
|
|
61
|
+
},
|
|
62
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { ensureApplet, ggbGetProperty, normalizeLabel, unwrapBridgeEnvelope } from './utils.js';
|
|
4
|
+
|
|
5
|
+
cli({
|
|
6
|
+
site: 'geogebra',
|
|
7
|
+
name: 'info',
|
|
8
|
+
access: 'read',
|
|
9
|
+
description: 'Get detailed properties of a GeoGebra object',
|
|
10
|
+
domain: 'www.geogebra.org',
|
|
11
|
+
strategy: Strategy.PUBLIC,
|
|
12
|
+
browser: true,
|
|
13
|
+
navigateBefore: false,
|
|
14
|
+
example: 'opencli geogebra info --name A',
|
|
15
|
+
args: [
|
|
16
|
+
{ name: 'name', required: true, help: 'Object label (e.g. A, c1, poly1)' },
|
|
17
|
+
],
|
|
18
|
+
columns: ['property', 'value'],
|
|
19
|
+
func: async (page, kwargs) => {
|
|
20
|
+
const objName = normalizeLabel(kwargs.name, 'name');
|
|
21
|
+
await ensureApplet(page);
|
|
22
|
+
|
|
23
|
+
let exists;
|
|
24
|
+
try {
|
|
25
|
+
exists = unwrapBridgeEnvelope(await page.evaluate(`
|
|
26
|
+
(name => {
|
|
27
|
+
try {
|
|
28
|
+
if (typeof ggbApplet === 'undefined' || typeof ggbApplet.getObjectType !== 'function') {
|
|
29
|
+
return { error: 'ggbApplet is not ready' };
|
|
30
|
+
}
|
|
31
|
+
return { ok: true, exists: ggbApplet.getObjectType(name) !== '' };
|
|
32
|
+
} catch (err) {
|
|
33
|
+
return { error: err?.message || String(err) };
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
(${JSON.stringify(objName)})
|
|
37
|
+
`));
|
|
38
|
+
} catch (err) {
|
|
39
|
+
throw new CommandExecutionError(`Failed to inspect GeoGebra object: ${err?.message || err}`);
|
|
40
|
+
}
|
|
41
|
+
if (!exists || typeof exists !== 'object' || Array.isArray(exists)) {
|
|
42
|
+
throw new CommandExecutionError('GeoGebra object existence probe returned malformed result');
|
|
43
|
+
}
|
|
44
|
+
if (exists.error) {
|
|
45
|
+
throw new CommandExecutionError(`Failed to inspect GeoGebra object: ${exists.error}`);
|
|
46
|
+
}
|
|
47
|
+
if (exists.ok !== true || typeof exists.exists !== 'boolean') {
|
|
48
|
+
throw new CommandExecutionError('GeoGebra object existence probe returned malformed result');
|
|
49
|
+
}
|
|
50
|
+
if (exists.exists === false) {
|
|
51
|
+
throw new EmptyResultError(`geogebra info ${objName}`, `Object "${objName}" not found on the canvas.`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const properties = ['type', 'value', 'definition', 'command', 'caption', 'visible', 'color'];
|
|
55
|
+
const rows = [];
|
|
56
|
+
for (const prop of properties) {
|
|
57
|
+
const val = await ggbGetProperty(page, objName, prop);
|
|
58
|
+
rows.push({ property: prop, value: String(val ?? '') });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// For point-like objects, also include coordinates
|
|
62
|
+
const objType = await ggbGetProperty(page, objName, 'type');
|
|
63
|
+
if (objType === 'point') {
|
|
64
|
+
const x = await ggbGetProperty(page, objName, 'xcoord');
|
|
65
|
+
const y = await ggbGetProperty(page, objName, 'ycoord');
|
|
66
|
+
rows.push({ property: 'x', value: String(x ?? '') });
|
|
67
|
+
rows.push({ property: 'y', value: String(y ?? '') });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return rows;
|
|
71
|
+
},
|
|
72
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { ArgumentError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { ensureApplet, ggbListObjects } from './utils.js';
|
|
4
|
+
|
|
5
|
+
cli({
|
|
6
|
+
site: 'geogebra',
|
|
7
|
+
name: 'list',
|
|
8
|
+
access: 'read',
|
|
9
|
+
description: 'List all geometric objects on the GeoGebra canvas',
|
|
10
|
+
domain: 'www.geogebra.org',
|
|
11
|
+
strategy: Strategy.PUBLIC,
|
|
12
|
+
browser: true,
|
|
13
|
+
navigateBefore: false,
|
|
14
|
+
args: [
|
|
15
|
+
{ name: 'type', required: false, help: 'Filter by object type (e.g. "point", "line", "circle")' },
|
|
16
|
+
],
|
|
17
|
+
columns: ['name', 'type', 'value', 'visible'],
|
|
18
|
+
func: async (page, kwargs) => {
|
|
19
|
+
const filterType = kwargs.type == null || kwargs.type === ''
|
|
20
|
+
? ''
|
|
21
|
+
: String(kwargs.type).trim().toLowerCase();
|
|
22
|
+
if (filterType && !/^[a-z-]+$/.test(filterType)) {
|
|
23
|
+
throw new ArgumentError('type must be a GeoGebra object type like point, line, or circle');
|
|
24
|
+
}
|
|
25
|
+
await ensureApplet(page);
|
|
26
|
+
const objects = await ggbListObjects(page, filterType);
|
|
27
|
+
if (!Array.isArray(objects) || objects.length === 0) {
|
|
28
|
+
throw new EmptyResultError(
|
|
29
|
+
'geogebra list',
|
|
30
|
+
'No objects found on the canvas. Fresh runs start a blank session; use one "eval" call, or inspect an already-bound tab through the browser workspace commands.',
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return objects;
|
|
34
|
+
},
|
|
35
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { CommandExecutionError } from '@jackwener/opencli/errors';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { ensureApplet, ggbEval, ggbListObjects, ggbWaitForObjectCount, normalizeNumber, requireGgbSuccess } from './utils.js';
|
|
6
|
+
|
|
7
|
+
cli({
|
|
8
|
+
site: 'geogebra',
|
|
9
|
+
name: 'triangle',
|
|
10
|
+
access: 'write',
|
|
11
|
+
description: 'Draw an equilateral triangle from a horizontal base segment',
|
|
12
|
+
domain: 'www.geogebra.org',
|
|
13
|
+
strategy: Strategy.PUBLIC,
|
|
14
|
+
browser: true,
|
|
15
|
+
navigateBefore: false,
|
|
16
|
+
example: 'opencli geogebra triangle --size 4',
|
|
17
|
+
args: [
|
|
18
|
+
{ name: 'size', required: false, default: '2', help: 'Side length of the triangle (default: 2)' },
|
|
19
|
+
],
|
|
20
|
+
columns: ['step', 'result'],
|
|
21
|
+
func: async (page, kwargs) => {
|
|
22
|
+
const size = normalizeNumber(kwargs.size, 'size', { defaultValue: 2, positive: true });
|
|
23
|
+
await ensureApplet(page);
|
|
24
|
+
const results = [];
|
|
25
|
+
|
|
26
|
+
const r1 = requireGgbSuccess(await ggbEval(page, 'A=(0,0)'), 'Failed to create point A');
|
|
27
|
+
results.push({ step: 'base point A=(0,0)', result: `ok (${r1.label || 'A'})` });
|
|
28
|
+
|
|
29
|
+
const r2 = requireGgbSuccess(await ggbEval(page, `B=(${size},0)`), 'Failed to create point B');
|
|
30
|
+
results.push({ step: `base point B=(${size},0)`, result: `ok (${r2.label || 'B'})` });
|
|
31
|
+
|
|
32
|
+
const r3 = requireGgbSuccess(await ggbEval(page, 'c=Circle(A,B)'), 'Failed to create circle c');
|
|
33
|
+
results.push({ step: 'c=Circle(A,B)', result: `ok (${r3.label || 'c'})` });
|
|
34
|
+
|
|
35
|
+
const r4 = requireGgbSuccess(await ggbEval(page, 'd=Circle(B,A)'), 'Failed to create circle d');
|
|
36
|
+
results.push({ step: 'd=Circle(B,A)', result: `ok (${r4.label || 'd'})` });
|
|
37
|
+
|
|
38
|
+
const r5 = requireGgbSuccess(await ggbEval(page, 'C=Intersect(c,d,1)'), 'Failed to create point C');
|
|
39
|
+
results.push({ step: 'C=Intersect(c,d,1)', result: `ok (${r5.label || 'C'})` });
|
|
40
|
+
|
|
41
|
+
const r6 = requireGgbSuccess(await ggbEval(page, 'Polygon(A,B,C)'), 'Failed to create triangle polygon');
|
|
42
|
+
results.push({ step: 'Polygon(A,B,C)', result: `ok (${r6.label || 'triangle created'})` });
|
|
43
|
+
|
|
44
|
+
const objectCount = await ggbWaitForObjectCount(page, 5);
|
|
45
|
+
const objects = await ggbListObjects(page);
|
|
46
|
+
const screenshotPath = path.join(os.tmpdir(), 'opencli-geogebra-triangle.png');
|
|
47
|
+
try {
|
|
48
|
+
await page.screenshot({ path: screenshotPath });
|
|
49
|
+
} catch (err) {
|
|
50
|
+
throw new CommandExecutionError(`Failed to capture GeoGebra screenshot: ${err?.message || err}`);
|
|
51
|
+
}
|
|
52
|
+
results.push({
|
|
53
|
+
step: `canvas has ${objectCount} objects`,
|
|
54
|
+
result: objects.map((obj) => `${obj.name}(${obj.type})`).join(', '),
|
|
55
|
+
});
|
|
56
|
+
results.push({ step: 'screenshot', result: screenshotPath });
|
|
57
|
+
|
|
58
|
+
return results;
|
|
59
|
+
},
|
|
60
|
+
});
|