@autobest-ui/agent 1.0.0 → 1.0.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.
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "Autobest Figma Plugin",
3
+ "id": "autobest-figma-plugin",
4
+ "api": "1.0.0",
5
+ "main": "code.js",
6
+ "ui": "ui.html",
7
+ "editorType": ["figma", "figjam", "dev"],
8
+ "capabilities": ["inspect"],
9
+ "documentAccess": "dynamic-page",
10
+ "networkAccess": {
11
+ "allowedDomains": ["none"],
12
+ "devAllowedDomains": [
13
+ "ws://localhost:3055",
14
+ "ws://localhost:3056",
15
+ "ws://localhost:3057",
16
+ "ws://localhost:3058",
17
+ "ws://localhost:3059",
18
+ "ws://localhost:3060",
19
+ "ws://localhost:3061",
20
+ "ws://localhost:3062",
21
+ "ws://localhost:3063",
22
+ "ws://localhost:3064",
23
+ "ws://localhost:3065",
24
+ "ws://localhost:3066",
25
+ "ws://localhost:3067",
26
+ "ws://localhost:3068",
27
+ "ws://localhost:3069",
28
+ "ws://localhost:3070"
29
+ ]
30
+ },
31
+ "permissions": ["currentuser"]
32
+ }
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { cp, mkdir, rm } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const sourceRoot = path.resolve(
9
+ path.dirname(fileURLToPath(import.meta.url)),
10
+ "..",
11
+ );
12
+ const agentHome = path.resolve(
13
+ process.env.AUTOBEST_AGENT_HOME || path.join(os.homedir(), ".autobest-agent"),
14
+ );
15
+ const pluginsRoot = path.join(agentHome, "figma-plugins");
16
+ const installRoot = path.join(pluginsRoot, "figma-plugin");
17
+ const pluginFiles = ["manifest.json", "code.js", "ui.html", "LICENSE"];
18
+
19
+ function printHelp() {
20
+ process.stdout.write(`Figma Plugin 安装器
21
+
22
+ 用法:
23
+ figma-plugin [install]
24
+ figma-plugin uninstall
25
+ figma-plugin --help
26
+ `);
27
+ }
28
+
29
+ function assertSafeInstallRoot() {
30
+ const expected = path.join(agentHome, "figma-plugins", "figma-plugin");
31
+ if (
32
+ installRoot !== expected ||
33
+ path.basename(installRoot) !== "figma-plugin" ||
34
+ path.dirname(installRoot) !== pluginsRoot
35
+ ) {
36
+ throw new Error(`拒绝操作非预期路径:${installRoot}`);
37
+ }
38
+ }
39
+
40
+ async function install() {
41
+ assertSafeInstallRoot();
42
+ await mkdir(installRoot, { recursive: true });
43
+ await Promise.all(
44
+ pluginFiles.map((file) =>
45
+ cp(path.join(sourceRoot, file), path.join(installRoot, file), {
46
+ force: true,
47
+ }),
48
+ ),
49
+ );
50
+ process.stdout.write(`Figma Plugin 已安装:${installRoot}\n`);
51
+ process.stdout.write(`请在 Figma 中导入:${path.join(installRoot, "manifest.json")}\n`);
52
+ }
53
+
54
+ async function uninstall() {
55
+ assertSafeInstallRoot();
56
+ await rm(installRoot, { recursive: true, force: true });
57
+ process.stdout.write(`Figma Plugin 本地文件已卸载:${installRoot}\n`);
58
+ }
59
+
60
+ const command = process.argv[2] || "install";
61
+
62
+ if (command === "--help" || command === "-h" || command === "help") {
63
+ printHelp();
64
+ } else if (command === "install") {
65
+ await install();
66
+ } else if (command === "uninstall") {
67
+ await uninstall();
68
+ } else {
69
+ process.stderr.write(`未知命令:${command}\n\n`);
70
+ printHelp();
71
+ process.exitCode = 1;
72
+ }
@@ -0,0 +1,45 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFile } from "node:child_process";
3
+ import { access, mkdtemp, readFile } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import test from "node:test";
7
+ import { promisify } from "node:util";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ const execFileAsync = promisify(execFile);
11
+ const setupPath = fileURLToPath(new URL("./setup.mjs", import.meta.url));
12
+
13
+ test("installs, updates, and uninstalls the Figma plugin in a stable directory", async () => {
14
+ const temporaryRoot = await mkdtemp(
15
+ path.join(os.tmpdir(), "autobest-figma-plugin-"),
16
+ );
17
+ const agentHome = path.join(temporaryRoot, "agent-home");
18
+ const env = { ...process.env, AUTOBEST_AGENT_HOME: agentHome };
19
+ const installRoot = path.join(
20
+ agentHome,
21
+ "figma-plugins",
22
+ "figma-plugin",
23
+ );
24
+
25
+ const installed = await execFileAsync(process.execPath, [setupPath], { env });
26
+ assert.match(installed.stdout, /Figma Plugin 已安装/);
27
+ assert.match(installed.stdout, /manifest\.json/);
28
+
29
+ const manifest = JSON.parse(
30
+ await readFile(path.join(installRoot, "manifest.json"), "utf8"),
31
+ );
32
+ assert.equal(manifest.id, "autobest-figma-plugin");
33
+ await access(path.join(installRoot, "code.js"));
34
+ await access(path.join(installRoot, "ui.html"));
35
+ await access(path.join(installRoot, "LICENSE"));
36
+
37
+ await execFileAsync(process.execPath, [setupPath, "install"], { env });
38
+ await execFileAsync(process.execPath, [setupPath, "uninstall"], { env });
39
+ await assert.rejects(access(installRoot));
40
+
41
+ const help = await execFileAsync(process.execPath, [setupPath, "--help"], {
42
+ env,
43
+ });
44
+ assert.match(help.stdout, /figma-plugin uninstall/);
45
+ });
@@ -0,0 +1,235 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <style>
5
+ body {
6
+ font-family: Inter, sans-serif;
7
+ font-size: 11px;
8
+ padding: 8px;
9
+ margin: 0;
10
+ background: #2c2c2c;
11
+ color: #fff;
12
+ }
13
+ .row {
14
+ display: flex;
15
+ align-items: center;
16
+ gap: 6px;
17
+ }
18
+ .port-label {
19
+ color: #999;
20
+ }
21
+ #port-input {
22
+ width: 60px;
23
+ padding: 4px 6px;
24
+ border-radius: 4px;
25
+ border: 1px solid #444;
26
+ background: #1e1e1e;
27
+ color: #fff;
28
+ font-size: 11px;
29
+ }
30
+ #port-input:focus {
31
+ outline: none;
32
+ border-color: #0d99ff;
33
+ }
34
+ #status-indicator {
35
+ font-size: 14px;
36
+ line-height: 1;
37
+ cursor: pointer;
38
+ user-select: none;
39
+ transition: color 0.15s;
40
+ }
41
+ #status-indicator.connected {
42
+ color: #2ecc71;
43
+ }
44
+ #status-indicator.disconnected,
45
+ #status-indicator.connecting {
46
+ color: #c2410c;
47
+ }
48
+ #readonly-badge {
49
+ color: #999;
50
+ border: 1px solid #444;
51
+ border-radius: 4px;
52
+ padding: 2px 5px;
53
+ white-space: nowrap;
54
+ }
55
+ </style>
56
+ </head>
57
+ <body>
58
+ <div class="row">
59
+ <span class="port-label">Port:</span>
60
+ <input
61
+ type="number"
62
+ id="port-input"
63
+ value="3055"
64
+ min="1024"
65
+ max="65535"
66
+ onchange="changePort()"
67
+ />
68
+ <span
69
+ id="status-indicator"
70
+ class="disconnected"
71
+ title="Disconnected — click to reconnect"
72
+ onclick="manualReconnect()"
73
+ >✓</span
74
+ >
75
+ <span
76
+ id="readonly-badge"
77
+ hidden
78
+ title="Dev Mode — the document is read-only, so only get/search/export tools work"
79
+ >read-only</span
80
+ >
81
+ </div>
82
+
83
+ <script>
84
+ const CONFIG = {
85
+ defaultPort: 3055,
86
+ reconnectMaxAttempts: 10,
87
+ reconnectBaseDelay: 1000,
88
+ reconnectMaxDelay: 30000,
89
+ };
90
+
91
+ let socket = null;
92
+ let reconnectAttempts = 0;
93
+ let reconnectTimer = null;
94
+ const indicator = document.getElementById("status-indicator");
95
+ const portInput = document.getElementById("port-input");
96
+
97
+ function getServerUrl() {
98
+ const port = parseInt(portInput.value, 10) || CONFIG.defaultPort;
99
+ return "ws://localhost:" + port;
100
+ }
101
+
102
+ function changePort() {
103
+ if (socket) {
104
+ socket.close(1000, "Port changed");
105
+ socket = null;
106
+ }
107
+ if (reconnectTimer) {
108
+ clearTimeout(reconnectTimer);
109
+ reconnectTimer = null;
110
+ }
111
+ reconnectAttempts = 0;
112
+ connect();
113
+ }
114
+
115
+ function setStatus(state, tooltip) {
116
+ indicator.className = state;
117
+ indicator.title = tooltip;
118
+ }
119
+
120
+ function manualReconnect() {
121
+ if (reconnectTimer) {
122
+ clearTimeout(reconnectTimer);
123
+ reconnectTimer = null;
124
+ }
125
+ reconnectAttempts = 0;
126
+ connect();
127
+ }
128
+
129
+ function connect() {
130
+ if (socket && socket.readyState === WebSocket.OPEN) return;
131
+
132
+ setStatus("connecting", "Connecting to port " + portInput.value + "…");
133
+
134
+ try {
135
+ socket = new WebSocket(getServerUrl());
136
+
137
+ socket.onopen = () => {
138
+ console.log("[UI] Connected to port " + portInput.value);
139
+ setStatus("connected", "Connected on port " + portInput.value);
140
+ reconnectAttempts = 0;
141
+ parent.postMessage(
142
+ { pluginMessage: { type: "get_handshake_info" } },
143
+ "*",
144
+ );
145
+ };
146
+
147
+ socket.onmessage = (event) => {
148
+ let message;
149
+ try {
150
+ message = JSON.parse(event.data);
151
+ } catch (e) {
152
+ console.error("[UI] Parse error:", e);
153
+ return;
154
+ }
155
+
156
+ if (message.type === "handshake_ack") {
157
+ console.log("[UI] Handshake acknowledged");
158
+ return;
159
+ }
160
+
161
+ if (message.type === "ping") {
162
+ socket.send(
163
+ JSON.stringify({ type: "pong", timestamp: message.timestamp }),
164
+ );
165
+ return;
166
+ }
167
+
168
+ if (message.requestId && message.command) {
169
+ parent.postMessage(
170
+ {
171
+ pluginMessage: {
172
+ type: "command",
173
+ requestId: message.requestId,
174
+ command: message.command,
175
+ payload: message.payload || {},
176
+ },
177
+ },
178
+ "*",
179
+ );
180
+ }
181
+ };
182
+
183
+ socket.onclose = (event) => {
184
+ console.log("[UI] Closed:", event.code);
185
+ setStatus("disconnected", "Disconnected — click to reconnect");
186
+ socket = null;
187
+ if (event.code !== 1000) scheduleReconnect();
188
+ };
189
+
190
+ socket.onerror = (e) => console.error("[UI] Error:", e);
191
+ } catch (error) {
192
+ console.error("[UI] Failed:", error);
193
+ scheduleReconnect();
194
+ }
195
+ }
196
+
197
+ function scheduleReconnect() {
198
+ if (reconnectAttempts >= CONFIG.reconnectMaxAttempts) {
199
+ setStatus("disconnected", "Failed — click to reconnect");
200
+ return;
201
+ }
202
+ const delay = Math.min(
203
+ CONFIG.reconnectBaseDelay * Math.pow(2, reconnectAttempts),
204
+ CONFIG.reconnectMaxDelay,
205
+ );
206
+ reconnectAttempts++;
207
+ setStatus("connecting", "Retry in " + delay / 1000 + "s — click to retry now");
208
+ reconnectTimer = setTimeout(connect, delay);
209
+ }
210
+
211
+ function sendToServer(data) {
212
+ if (socket && socket.readyState === WebSocket.OPEN) {
213
+ socket.send(JSON.stringify(data));
214
+ }
215
+ }
216
+
217
+ window.onmessage = (event) => {
218
+ const msg = event.data.pluginMessage;
219
+ if (!msg) return;
220
+
221
+ if (msg.type === "handshake_info") {
222
+ sendToServer({ type: "handshake", payload: msg.payload });
223
+ } else if (msg.type === "command_response") {
224
+ sendToServer({ responseTo: msg.requestId, payload: msg.payload });
225
+ } else if (msg.type === "editor_info") {
226
+ document.getElementById("readonly-badge").hidden =
227
+ !msg.payload.readOnly;
228
+ }
229
+ };
230
+
231
+ parent.postMessage({ pluginMessage: { type: "get_editor_info" } }, "*");
232
+ connect();
233
+ </script>
234
+ </body>
235
+ </html>