@await-widget/runtime 0.0.23 → 0.0.25
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/bin/await-widget.js +749 -103
- package/package.json +1 -1
- package/types/model.d.ts +6 -0
package/bin/await-widget.js
CHANGED
|
@@ -1,124 +1,475 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
2
|
+
/* eslint-disable n/prefer-global/buffer, no-bitwise, unicorn/no-array-sort */
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import http from 'node:http';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import process from 'node:process';
|
|
7
9
|
|
|
8
10
|
const defaultPort = 4343;
|
|
11
|
+
const maxBodyBytes = 100 * 1024 * 1024;
|
|
9
12
|
const zipDosTime = 0;
|
|
10
13
|
const zipDosDate = 33;
|
|
11
|
-
const excludedNames = new Set([
|
|
14
|
+
const excludedNames = new Set(['.git', '.build', 'node_modules', 'dist', 'build']);
|
|
12
15
|
const crcTable = makeCrcTable();
|
|
16
|
+
const bridgeInfoDirectory = '.await';
|
|
17
|
+
const bridgeInfoFileName = 'bridge.json';
|
|
18
|
+
const singleWidgetPath = '.';
|
|
13
19
|
|
|
14
20
|
function main() {
|
|
15
|
-
const
|
|
16
|
-
if (
|
|
17
|
-
|
|
21
|
+
const options = parseArgs(process.argv.slice(2));
|
|
22
|
+
if (options.help) {
|
|
23
|
+
printHelp();
|
|
18
24
|
return;
|
|
19
25
|
}
|
|
20
|
-
printHelp();
|
|
21
|
-
}
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
const stats = fs.statSync(root);
|
|
31
|
-
if (!stats.isDirectory()) {
|
|
32
|
-
throw new Error(`${root} is not a directory`);
|
|
27
|
+
if (options.command === 'app') {
|
|
28
|
+
runAppCommand(options).catch(error => {
|
|
29
|
+
console.error(error.message);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
});
|
|
32
|
+
return;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
const server = http.createServer((request, response) => {
|
|
37
|
-
handleRequest({request, response, root, token});
|
|
38
|
-
});
|
|
39
|
-
server.listen(options.port, "0.0.0.0", () => {
|
|
40
|
-
const address = server.address();
|
|
41
|
-
const port = typeof address === "object" && address ? address.port : options.port;
|
|
42
|
-
console.log(`Await dev server serving ${root}`);
|
|
43
|
-
printServerUrls(port, token);
|
|
44
|
-
console.log("Press Ctrl+C to stop.");
|
|
45
|
-
});
|
|
35
|
+
startWorkspaceServer(options);
|
|
46
36
|
}
|
|
47
37
|
|
|
48
|
-
function
|
|
49
|
-
|
|
38
|
+
function parseArgs(args) {
|
|
39
|
+
if (args[0] === 'app') {
|
|
40
|
+
return parseAppArgs(args.slice(1));
|
|
41
|
+
}
|
|
42
|
+
|
|
50
43
|
let port = defaultPort;
|
|
51
44
|
for (let index = 0; index < args.length; index += 1) {
|
|
52
45
|
const arg = args[index];
|
|
53
|
-
if (arg ===
|
|
46
|
+
if (arg === '--help' || arg === '-h') {
|
|
47
|
+
return {help: true, port};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (arg === '--port') {
|
|
54
51
|
port = Number(args[index + 1]);
|
|
55
52
|
index += 1;
|
|
56
53
|
continue;
|
|
57
54
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
root = arg;
|
|
55
|
+
|
|
56
|
+
printHelp();
|
|
57
|
+
process.exit(1);
|
|
63
58
|
}
|
|
64
|
-
|
|
59
|
+
|
|
60
|
+
return {command: 'bridge', help: false, port};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseAppArgs(args) {
|
|
64
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
65
|
+
return {command: 'app', help: true};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const tool = appCommandName(args[0]);
|
|
69
|
+
let bridgeUrl;
|
|
70
|
+
let workspace;
|
|
71
|
+
const toolArguments = {};
|
|
72
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
73
|
+
const arg = args[index];
|
|
74
|
+
if (arg === '--bridge-url' || arg === '--url') {
|
|
75
|
+
bridgeUrl = args[index + 1];
|
|
76
|
+
index += 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (arg === '--workspace') {
|
|
81
|
+
workspace = args[index + 1];
|
|
82
|
+
index += 1;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (arg === '--json') {
|
|
87
|
+
Object.assign(toolArguments, JSON.parse(args[index + 1]));
|
|
88
|
+
index += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (arg === '--widget-id') {
|
|
93
|
+
toolArguments.widgetId = args[index + 1];
|
|
94
|
+
index += 1;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (arg === '--mode') {
|
|
99
|
+
toolArguments.mode = args[index + 1];
|
|
100
|
+
index += 1;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (arg === '--timeout-ms') {
|
|
105
|
+
toolArguments.timeoutMs = Number(args[index + 1]);
|
|
106
|
+
index += 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (arg === '--intent-id') {
|
|
111
|
+
toolArguments.intentId = args[index + 1];
|
|
112
|
+
index += 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (arg === '--input') {
|
|
117
|
+
toolArguments.input = JSON.parse(args[index + 1]);
|
|
118
|
+
index += 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
65
122
|
printHelp();
|
|
66
123
|
process.exit(1);
|
|
67
124
|
}
|
|
68
|
-
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
command: 'app', help: false, bridgeUrl, workspace, tool, toolArguments,
|
|
128
|
+
};
|
|
69
129
|
}
|
|
70
130
|
|
|
71
|
-
function
|
|
72
|
-
|
|
73
|
-
|
|
131
|
+
function startWorkspaceServer(options) {
|
|
132
|
+
let roots;
|
|
133
|
+
try {
|
|
134
|
+
roots = resolveWidgetServerRoots(process.cwd());
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error(error.message);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const {packageRoot, widgetRoot} = roots;
|
|
141
|
+
|
|
142
|
+
const token = crypto.randomBytes(6).toString('hex');
|
|
143
|
+
const bridge = createBridgeState(widgetRoot);
|
|
144
|
+
const server = http.createServer((request, response) => {
|
|
145
|
+
handleRequest({
|
|
146
|
+
request, response, root: widgetRoot, token, bridge,
|
|
147
|
+
}).catch(error => {
|
|
148
|
+
sendJson(response, {error: error.message}, 500);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
startWorkspaceWatcher(widgetRoot, bridge);
|
|
152
|
+
server.listen(options.port, '0.0.0.0', () => {
|
|
153
|
+
const address = server.address();
|
|
154
|
+
const port = typeof address === 'object' && address ? address.port : options.port;
|
|
155
|
+
writeBridgeInfo(packageRoot, port, token);
|
|
156
|
+
console.log(`Await computer connection serving ${widgetRoot}`);
|
|
157
|
+
printServerUrls(port, token);
|
|
158
|
+
console.log('Press Ctrl+C to stop.');
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function createBridgeState(root) {
|
|
163
|
+
return {
|
|
164
|
+
events: new Set(),
|
|
165
|
+
activeEvent: undefined,
|
|
166
|
+
pendingCommands: new Map(),
|
|
167
|
+
widgetChangeTimers: new Map(),
|
|
168
|
+
widgetsChangedTimer: undefined,
|
|
169
|
+
knownWidgets: widgetPathKey(listDesktopWidgets(root)),
|
|
170
|
+
};
|
|
74
171
|
}
|
|
75
172
|
|
|
76
|
-
function handleRequest({request, response, root, token}) {
|
|
77
|
-
const url = new URL(request.url ??
|
|
78
|
-
if (url.pathname ===
|
|
79
|
-
sendText(response, `Await
|
|
173
|
+
async function handleRequest({request, response, root, token, bridge}) {
|
|
174
|
+
const url = new URL(request.url ?? '/', 'http://localhost');
|
|
175
|
+
if (url.pathname === '/') {
|
|
176
|
+
sendText(response, `Await computer connection\n${urlForRequest(request, token)}\n`);
|
|
80
177
|
return;
|
|
81
178
|
}
|
|
82
|
-
|
|
83
|
-
|
|
179
|
+
|
|
180
|
+
if (url.searchParams.get('token') !== token) {
|
|
181
|
+
sendText(response, 'Forbidden\n', 403);
|
|
84
182
|
return;
|
|
85
183
|
}
|
|
86
|
-
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
await routeRequest({
|
|
187
|
+
request, response, root, url, bridge,
|
|
188
|
+
});
|
|
189
|
+
} catch (error) {
|
|
190
|
+
sendJson(response, {error: error.message}, errorStatus(error));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function routeRequest({request, response, root, url, bridge}) {
|
|
195
|
+
if (request.method === 'GET' && url.pathname === '/await-dev/connection') {
|
|
87
196
|
sendJson(response, {
|
|
88
|
-
|
|
89
|
-
|
|
197
|
+
workspaceName: path.basename(root),
|
|
198
|
+
capabilities: {
|
|
199
|
+
events: true,
|
|
200
|
+
appCommands: true,
|
|
201
|
+
appMcp: true,
|
|
202
|
+
singleWidget: true,
|
|
203
|
+
},
|
|
204
|
+
widgets: listDesktopWidgets(root),
|
|
90
205
|
});
|
|
91
206
|
return;
|
|
92
207
|
}
|
|
93
|
-
|
|
94
|
-
|
|
208
|
+
|
|
209
|
+
if (request.method === 'GET' && url.pathname === '/await-dev/widgets') {
|
|
210
|
+
sendJson(response, {widgets: listDesktopWidgets(root)});
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (request.method === 'GET' && url.pathname === '/await-dev/events') {
|
|
215
|
+
addEventClient(bridge, response);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (request.method === 'GET' && url.pathname === '/await-dev/version') {
|
|
220
|
+
const widgetRoot = widgetRootFromQuery(root, url);
|
|
221
|
+
console.log(`Await version requested: ${path.basename(widgetRoot)}`);
|
|
222
|
+
sendJson(response, {
|
|
223
|
+
name: path.basename(widgetRoot),
|
|
224
|
+
path: singleWidgetPath,
|
|
225
|
+
version: projectVersion(widgetRoot),
|
|
226
|
+
});
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (request.method === 'GET' && url.pathname === '/await-dev/snapshot.zip') {
|
|
231
|
+
const widgetRoot = widgetRootFromQuery(root, url);
|
|
232
|
+
console.log(`Await snapshot requested: ${path.basename(widgetRoot)}`);
|
|
233
|
+
const archive = zipFiles(scanFiles(widgetRoot));
|
|
95
234
|
response.writeHead(200, {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
235
|
+
'Content-Type': 'application/zip',
|
|
236
|
+
'Content-Length': archive.length,
|
|
237
|
+
'Cache-Control': 'no-store',
|
|
99
238
|
});
|
|
100
239
|
response.end(archive);
|
|
101
240
|
return;
|
|
102
241
|
}
|
|
103
|
-
|
|
242
|
+
|
|
243
|
+
if (request.method === 'POST' && url.pathname === '/await-dev/app-command') {
|
|
244
|
+
const body = await readJsonBody(request);
|
|
245
|
+
const result = await sendAppCommand(bridge, body.tool, body.arguments ?? {});
|
|
246
|
+
sendJson(response, result);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (request.method === 'POST' && url.pathname === '/await-dev/command-result') {
|
|
251
|
+
const body = await readJsonBody(request);
|
|
252
|
+
completeAppCommand(bridge, body);
|
|
253
|
+
sendJson(response, {ok: true});
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
sendText(response, 'Not Found\n', 404);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function addEventClient(bridge, response) {
|
|
261
|
+
response.writeHead(200, {
|
|
262
|
+
'Content-Type': 'text/event-stream',
|
|
263
|
+
'Cache-Control': 'no-store',
|
|
264
|
+
Connection: 'keep-alive',
|
|
265
|
+
});
|
|
266
|
+
response.flushHeaders?.();
|
|
267
|
+
response.write(': connected\n\n');
|
|
268
|
+
console.log('Await app event stream connected.');
|
|
269
|
+
const heartbeat = setInterval(() => {
|
|
270
|
+
response.write(': ping\n\n');
|
|
271
|
+
}, 15_000);
|
|
272
|
+
bridge.events.add(response);
|
|
273
|
+
bridge.activeEvent = response;
|
|
274
|
+
response.on('close', () => {
|
|
275
|
+
clearInterval(heartbeat);
|
|
276
|
+
bridge.events.delete(response);
|
|
277
|
+
if (bridge.activeEvent === response) {
|
|
278
|
+
bridge.activeEvent = [...bridge.events].at(-1);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
console.log('Await app event stream disconnected.');
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function writeEvent(response, event, data) {
|
|
286
|
+
response.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function sendEvent(bridge, event, data) {
|
|
290
|
+
if (event === 'widgetChanged' || event === 'widgetsChanged') {
|
|
291
|
+
console.log(`Await event ${event}: ${JSON.stringify(data)}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
for (const response of bridge.events) {
|
|
295
|
+
writeEvent(response, event, data);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function sendAppCommand(bridge, tool, args) {
|
|
300
|
+
if (!bridge.activeEvent) {
|
|
301
|
+
throw errorWithStatus('Await app is not connected.', 503);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const id = crypto.randomUUID();
|
|
305
|
+
const command = {id, tool, arguments: args};
|
|
306
|
+
return new Promise((resolve, reject) => {
|
|
307
|
+
const timer = setTimeout(() => {
|
|
308
|
+
bridge.pendingCommands.delete(id);
|
|
309
|
+
reject(errorWithStatus(`Await app command timed out: ${tool}`, 504));
|
|
310
|
+
}, 15_000);
|
|
311
|
+
bridge.pendingCommands.set(id, {resolve, reject, timer});
|
|
312
|
+
writeEvent(bridge.activeEvent, 'command', command);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function completeAppCommand(bridge, body) {
|
|
317
|
+
const id = String(body.id ?? '');
|
|
318
|
+
const pending = bridge.pendingCommands.get(id);
|
|
319
|
+
if (!pending) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
clearTimeout(pending.timer);
|
|
324
|
+
bridge.pendingCommands.delete(id);
|
|
325
|
+
if (body.ok === false) {
|
|
326
|
+
pending.reject(errorWithStatus(String(body.error ?? 'Await app command failed.'), 500));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
pending.resolve(body.result ?? {});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function startWorkspaceWatcher(root, bridge) {
|
|
334
|
+
try {
|
|
335
|
+
fs.watch(root, {recursive: true}, (_eventType, filename) => {
|
|
336
|
+
handleWorkspaceFileEvent(root, bridge, filename);
|
|
337
|
+
});
|
|
338
|
+
} catch {
|
|
339
|
+
fs.watch(root, (_eventType, filename) => {
|
|
340
|
+
handleWorkspaceFileEvent(root, bridge, filename);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function handleWorkspaceFileEvent(root, bridge, filename) {
|
|
346
|
+
const relativePath = String(filename ?? '').replaceAll('\\', '/');
|
|
347
|
+
const [topLevel] = relativePath.split('/');
|
|
348
|
+
if (topLevel && !isExcludedName(topLevel)) {
|
|
349
|
+
scheduleWidgetChanged(bridge, singleWidgetPath);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
scheduleWidgetsChanged(root, bridge);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function scheduleWidgetChanged(bridge, widgetPath) {
|
|
356
|
+
clearTimeout(bridge.widgetChangeTimers.get(widgetPath));
|
|
357
|
+
bridge.widgetChangeTimers.set(widgetPath, setTimeout(() => {
|
|
358
|
+
bridge.widgetChangeTimers.delete(widgetPath);
|
|
359
|
+
sendEvent(bridge, 'widgetChanged', {path: widgetPath});
|
|
360
|
+
}, 250));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function scheduleWidgetsChanged(root, bridge) {
|
|
364
|
+
clearTimeout(bridge.widgetsChangedTimer);
|
|
365
|
+
bridge.widgetsChangedTimer = setTimeout(() => {
|
|
366
|
+
const key = widgetPathKey(listDesktopWidgets(root));
|
|
367
|
+
if (key === bridge.knownWidgets) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
bridge.knownWidgets = key;
|
|
372
|
+
sendEvent(bridge, 'widgetsChanged', {widgets: listDesktopWidgets(root)});
|
|
373
|
+
}, 250);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function widgetPathKey(widgets) {
|
|
377
|
+
return widgets.map(widget => widget.path).sort((left, right) => left.localeCompare(right)).join('\n');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function findPackageRoot(start) {
|
|
381
|
+
let current = path.resolve(start);
|
|
382
|
+
while (true) {
|
|
383
|
+
const candidate = path.join(current, 'package.json');
|
|
384
|
+
if (fs.existsSync(candidate)) {
|
|
385
|
+
return current;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const parent = path.dirname(current);
|
|
389
|
+
if (parent === current) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
current = parent;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function resolveWidgetServerRoots(start) {
|
|
398
|
+
const widgetRoot = path.resolve(start);
|
|
399
|
+
const packageRoot = findPackageRoot(widgetRoot);
|
|
400
|
+
if (
|
|
401
|
+
!packageRoot
|
|
402
|
+
|| packageRoot === widgetRoot
|
|
403
|
+
|| path.dirname(widgetRoot) !== packageRoot
|
|
404
|
+
|| !isValidTopLevelName(path.basename(widgetRoot))
|
|
405
|
+
|| !hasRuntimeDependency(packageRoot)
|
|
406
|
+
) {
|
|
407
|
+
throw new Error('Run await-widget from a first-level widget folder inside a package whose package.json includes @await-widget/runtime.');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return {packageRoot, widgetRoot};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function hasRuntimeDependency(packageRoot) {
|
|
414
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
415
|
+
return Boolean(manifest.dependencies?.['@await-widget/runtime'] ?? manifest.devDependencies?.['@await-widget/runtime']);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function listDesktopWidgets(root) {
|
|
419
|
+
return [{name: path.basename(root), path: singleWidgetPath}];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function isExcludedName(name) {
|
|
423
|
+
return name.startsWith('.') || excludedNames.has(name);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function widgetRootFromQuery(root, url) {
|
|
427
|
+
const widgetPath = requiredSearchParam(url, 'path');
|
|
428
|
+
return safeWidgetPath(root, widgetPath);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function requiredSearchParam(url, name) {
|
|
432
|
+
const value = url.searchParams.get(name);
|
|
433
|
+
if (!value) {
|
|
434
|
+
throw new Error(`Missing ${name}`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return value;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function safeWidgetPath(root, widgetPath, options = {}) {
|
|
441
|
+
if (widgetPath !== singleWidgetPath) {
|
|
442
|
+
throw new Error(`Invalid widget path: ${widgetPath}`);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (!options.allowMissing && !isDirectory(root)) {
|
|
446
|
+
throw errorWithStatus(`Widget directory does not exist: ${widgetPath}`, 404);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return root;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function isValidTopLevelName(name) {
|
|
453
|
+
return Boolean(name) && !name.includes('/') && !name.includes('\\') && !isExcludedName(name);
|
|
104
454
|
}
|
|
105
455
|
|
|
106
456
|
function projectVersion(root) {
|
|
107
|
-
const hash = crypto.createHash(
|
|
457
|
+
const hash = crypto.createHash('sha256');
|
|
108
458
|
for (const file of scanFiles(root)) {
|
|
109
459
|
hash.update(file.relativePath);
|
|
110
|
-
hash.update(
|
|
460
|
+
hash.update('\0');
|
|
111
461
|
hash.update(String(file.size));
|
|
112
|
-
hash.update(
|
|
113
|
-
hash.update(String(
|
|
114
|
-
hash.update(
|
|
462
|
+
hash.update('\0');
|
|
463
|
+
hash.update(String(file.mtimeMs));
|
|
464
|
+
hash.update('\0');
|
|
115
465
|
}
|
|
116
|
-
|
|
466
|
+
|
|
467
|
+
return `sha256-${hash.digest('hex')}`;
|
|
117
468
|
}
|
|
118
469
|
|
|
119
470
|
function scanFiles(root) {
|
|
120
471
|
const files = [];
|
|
121
|
-
walk(root,
|
|
472
|
+
walk(root, '', files);
|
|
122
473
|
return files;
|
|
123
474
|
}
|
|
124
475
|
|
|
@@ -126,18 +477,21 @@ function walk(directory, relativeDirectory, files) {
|
|
|
126
477
|
const entries = fs.readdirSync(directory, {withFileTypes: true})
|
|
127
478
|
.sort((left, right) => left.name.localeCompare(right.name));
|
|
128
479
|
for (const entry of entries) {
|
|
129
|
-
if (
|
|
480
|
+
if (isExcludedName(entry.name)) {
|
|
130
481
|
continue;
|
|
131
482
|
}
|
|
483
|
+
|
|
132
484
|
const relativePath = relativeDirectory ? `${relativeDirectory}/${entry.name}` : entry.name;
|
|
133
485
|
const absolutePath = path.join(directory, entry.name);
|
|
134
486
|
if (entry.isDirectory()) {
|
|
135
487
|
walk(absolutePath, relativePath, files);
|
|
136
488
|
continue;
|
|
137
489
|
}
|
|
490
|
+
|
|
138
491
|
if (!entry.isFile()) {
|
|
139
492
|
continue;
|
|
140
493
|
}
|
|
494
|
+
|
|
141
495
|
const stats = fs.statSync(absolutePath);
|
|
142
496
|
files.push({
|
|
143
497
|
absolutePath,
|
|
@@ -153,13 +507,13 @@ function zipFiles(files) {
|
|
|
153
507
|
const centralParts = [];
|
|
154
508
|
let offset = 0;
|
|
155
509
|
for (const file of files) {
|
|
156
|
-
const name = Buffer.from(file.relativePath,
|
|
510
|
+
const name = Buffer.from(file.relativePath, 'utf8');
|
|
157
511
|
const data = fs.readFileSync(file.absolutePath);
|
|
158
512
|
const crc = crc32(data);
|
|
159
513
|
const localHeader = Buffer.concat([
|
|
160
|
-
u32(
|
|
514
|
+
u32(0x04_03_4B_50),
|
|
161
515
|
u16(20),
|
|
162
|
-
u16(
|
|
516
|
+
u16(0x08_00),
|
|
163
517
|
u16(0),
|
|
164
518
|
u16(zipDosTime),
|
|
165
519
|
u16(zipDosDate),
|
|
@@ -172,10 +526,10 @@ function zipFiles(files) {
|
|
|
172
526
|
]);
|
|
173
527
|
localParts.push(localHeader, data);
|
|
174
528
|
centralParts.push(Buffer.concat([
|
|
175
|
-
u32(
|
|
529
|
+
u32(0x02_01_4B_50),
|
|
176
530
|
u16(20),
|
|
177
531
|
u16(20),
|
|
178
|
-
u16(
|
|
532
|
+
u16(0x08_00),
|
|
179
533
|
u16(0),
|
|
180
534
|
u16(zipDosTime),
|
|
181
535
|
u16(zipDosDate),
|
|
@@ -193,9 +547,10 @@ function zipFiles(files) {
|
|
|
193
547
|
]));
|
|
194
548
|
offset += localHeader.length + data.length;
|
|
195
549
|
}
|
|
550
|
+
|
|
196
551
|
const centralDirectory = Buffer.concat(centralParts);
|
|
197
552
|
const end = Buffer.concat([
|
|
198
|
-
u32(
|
|
553
|
+
u32(0x06_05_4B_50),
|
|
199
554
|
u16(0),
|
|
200
555
|
u16(0),
|
|
201
556
|
u16(files.length),
|
|
@@ -207,24 +562,239 @@ function zipFiles(files) {
|
|
|
207
562
|
return Buffer.concat([...localParts, centralDirectory, end]);
|
|
208
563
|
}
|
|
209
564
|
|
|
565
|
+
const appCommands = [
|
|
566
|
+
{
|
|
567
|
+
name: 'list_app_widgets',
|
|
568
|
+
usage: 'list-app-widgets',
|
|
569
|
+
description: 'List Await widgets currently stored in the Await app.',
|
|
570
|
+
inputSchema: {type: 'object', properties: {}},
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
name: 'open_widget_detail',
|
|
574
|
+
usage: 'open-widget-detail --widget-id <id>',
|
|
575
|
+
description: 'Open a widget detail page in the Await app.',
|
|
576
|
+
inputSchema: {
|
|
577
|
+
type: 'object',
|
|
578
|
+
properties: {widgetId: {type: 'string'}},
|
|
579
|
+
required: ['widgetId'],
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
name: 'open_syncing_widget_detail',
|
|
584
|
+
usage: 'open-syncing-widget-detail',
|
|
585
|
+
description: 'Open the widget detail page for the widget currently syncing from this computer.',
|
|
586
|
+
inputSchema: {type: 'object', properties: {}},
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
name: 'set_preview_mode',
|
|
590
|
+
usage: 'set-preview-mode --mode <small|medium|large|extraLarge> [--widget-id <id>]',
|
|
591
|
+
description: 'Set the current widget detail preview mode.',
|
|
592
|
+
inputSchema: {
|
|
593
|
+
type: 'object',
|
|
594
|
+
properties: {
|
|
595
|
+
widgetId: {type: 'string'},
|
|
596
|
+
mode: {type: 'string', enum: ['small', 'medium', 'large', 'extraLarge']},
|
|
597
|
+
},
|
|
598
|
+
required: ['mode'],
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
name: 'capture_current_preview',
|
|
603
|
+
usage: 'capture-current-preview [--widget-id <id>]',
|
|
604
|
+
description: 'Capture the currently visible widget preview as PNG.',
|
|
605
|
+
inputSchema: {
|
|
606
|
+
type: 'object',
|
|
607
|
+
properties: {widgetId: {type: 'string'}},
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
name: 'get_widget_status',
|
|
612
|
+
usage: 'get-widget-status --widget-id <id>',
|
|
613
|
+
description: 'Get App-side status for a widget.',
|
|
614
|
+
inputSchema: {
|
|
615
|
+
type: 'object',
|
|
616
|
+
properties: {widgetId: {type: 'string'}},
|
|
617
|
+
required: ['widgetId'],
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
name: 'wait_for_widget_ready',
|
|
622
|
+
usage: 'wait-for-widget-ready --widget-id <id> [--timeout-ms <ms>]',
|
|
623
|
+
description: 'Wait for the current widget preview to have rendered data.',
|
|
624
|
+
inputSchema: {
|
|
625
|
+
type: 'object',
|
|
626
|
+
properties: {
|
|
627
|
+
widgetId: {type: 'string'},
|
|
628
|
+
timeoutMs: {type: 'number'},
|
|
629
|
+
},
|
|
630
|
+
required: ['widgetId'],
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
name: 'get_build_errors',
|
|
635
|
+
usage: 'get-build-errors --widget-id <id>',
|
|
636
|
+
description: 'Get recent build errors for a widget.',
|
|
637
|
+
inputSchema: {
|
|
638
|
+
type: 'object',
|
|
639
|
+
properties: {widgetId: {type: 'string'}},
|
|
640
|
+
required: ['widgetId'],
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
name: 'list_widget_intents',
|
|
645
|
+
usage: 'list-widget-intents --widget-id <id>',
|
|
646
|
+
description: 'List widget intents registered by a widget.',
|
|
647
|
+
inputSchema: {
|
|
648
|
+
type: 'object',
|
|
649
|
+
properties: {widgetId: {type: 'string'}},
|
|
650
|
+
required: ['widgetId'],
|
|
651
|
+
},
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
name: 'call_widget_intent',
|
|
655
|
+
usage: 'call-widget-intent --widget-id <id> --intent-id <id> [--input <json-array>]',
|
|
656
|
+
description: 'Call a widget intent with positional input arguments.',
|
|
657
|
+
inputSchema: {
|
|
658
|
+
type: 'object',
|
|
659
|
+
properties: {
|
|
660
|
+
widgetId: {type: 'string'},
|
|
661
|
+
intentId: {type: 'string'},
|
|
662
|
+
input: {
|
|
663
|
+
type: 'array',
|
|
664
|
+
items: {},
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
required: ['widgetId', 'intentId'],
|
|
668
|
+
},
|
|
669
|
+
},
|
|
670
|
+
];
|
|
671
|
+
|
|
672
|
+
function appCommandName(value) {
|
|
673
|
+
const name = value.replaceAll('-', '_');
|
|
674
|
+
if (!appCommands.some(command => command.name === name)) {
|
|
675
|
+
throw new Error(`Unsupported Await app command: ${value}`);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return name;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
async function runAppCommand(options) {
|
|
682
|
+
const result = await callAppCommand(resolveBridgeUrl(options), options.tool, options.toolArguments);
|
|
683
|
+
console.log(JSON.stringify(appCommandResult(options, options.tool, result), null, '\t'));
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function resolveBridgeUrl(options) {
|
|
687
|
+
if (options.bridgeUrl) {
|
|
688
|
+
return options.bridgeUrl;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const root = findPackageRoot(path.resolve(options.workspace ?? process.cwd()));
|
|
692
|
+
if (!root) {
|
|
693
|
+
throw new Error('Cannot find package.json for Await bridge lookup.');
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return readBridgeUrl(bridgeInfoPath(root));
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function readBridgeUrl(infoPath) {
|
|
700
|
+
if (!fs.existsSync(infoPath)) {
|
|
701
|
+
throw new Error('Cannot find Await bridge info. Run npx await-widget first.');
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const info = JSON.parse(fs.readFileSync(infoPath, 'utf8'));
|
|
705
|
+
return info.bridgeUrl;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
async function callAppCommand(bridgeUrl, tool, args) {
|
|
709
|
+
if (!appCommands.some(item => item.name === tool)) {
|
|
710
|
+
throw new Error(`Unsupported Await app command: ${tool}`);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const url = bridgeEndpoint(bridgeUrl, '/await-dev/app-command');
|
|
714
|
+
const response = await fetch(url, {
|
|
715
|
+
method: 'POST',
|
|
716
|
+
headers: {'Content-Type': 'application/json'},
|
|
717
|
+
body: JSON.stringify({tool, arguments: args}),
|
|
718
|
+
});
|
|
719
|
+
const body = await response.json();
|
|
720
|
+
if (!response.ok) {
|
|
721
|
+
throw new Error(body.error ?? `Await bridge request failed: ${response.status}`);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
return body;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function appCommandResult(options, tool, result) {
|
|
728
|
+
if (tool === 'capture_current_preview' && result.imageBase64) {
|
|
729
|
+
const filePath = saveCapture(options, result);
|
|
730
|
+
return {...result, imageBase64: undefined, filePath};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
return result;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function saveCapture(options, result) {
|
|
737
|
+
const root = findPackageRoot(options.workspace ? path.resolve(options.workspace) : process.cwd()) ?? process.cwd();
|
|
738
|
+
const directory = path.join(root, bridgeInfoDirectory, 'captures');
|
|
739
|
+
const name = safeCaptureFileName(String(result.widgetId ?? 'widget'));
|
|
740
|
+
const filePath = path.join(directory, `${name}-${Date.now()}.png`);
|
|
741
|
+
fs.mkdirSync(directory, {recursive: true});
|
|
742
|
+
fs.writeFileSync(filePath, Buffer.from(result.imageBase64, 'base64'));
|
|
743
|
+
return filePath;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function safeCaptureFileName(value) {
|
|
747
|
+
const name = value.replaceAll(/[^\w.-]+/g, '-').replaceAll(/^-+|-+$/g, '');
|
|
748
|
+
return name || 'widget';
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function bridgeEndpoint(bridgeUrl, pathname) {
|
|
752
|
+
const url = new URL(bridgeUrl);
|
|
753
|
+
url.pathname = pathname;
|
|
754
|
+
return url;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
async function readJsonBody(request) {
|
|
758
|
+
const body = await readBody(request);
|
|
759
|
+
return JSON.parse(body.toString('utf8'));
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async function readBody(request) {
|
|
763
|
+
const chunks = [];
|
|
764
|
+
let size = 0;
|
|
765
|
+
for await (const chunk of request) {
|
|
766
|
+
size += chunk.length;
|
|
767
|
+
if (size > maxBodyBytes) {
|
|
768
|
+
throw new Error('Request body too large');
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
chunks.push(chunk);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return Buffer.concat(chunks);
|
|
775
|
+
}
|
|
776
|
+
|
|
210
777
|
function makeCrcTable() {
|
|
211
778
|
const table = new Uint32Array(256);
|
|
212
779
|
for (let index = 0; index < table.length; index += 1) {
|
|
213
780
|
let value = index;
|
|
214
781
|
for (let bit = 0; bit < 8; bit += 1) {
|
|
215
|
-
value = value & 1 ?
|
|
782
|
+
value = value & 1 ? 0xED_B8_83_20 ^ (value >>> 1) : value >>> 1;
|
|
216
783
|
}
|
|
784
|
+
|
|
217
785
|
table[index] = value >>> 0;
|
|
218
786
|
}
|
|
787
|
+
|
|
219
788
|
return table;
|
|
220
789
|
}
|
|
221
790
|
|
|
222
791
|
function crc32(data) {
|
|
223
|
-
let crc =
|
|
792
|
+
let crc = 0xFF_FF_FF_FF;
|
|
224
793
|
for (const byte of data) {
|
|
225
794
|
crc = crcTable[(crc ^ byte) & 0xFF] ^ (crc >>> 8);
|
|
226
795
|
}
|
|
227
|
-
|
|
796
|
+
|
|
797
|
+
return (crc ^ 0xFF_FF_FF_FF) >>> 0;
|
|
228
798
|
}
|
|
229
799
|
|
|
230
800
|
function u16(value) {
|
|
@@ -239,50 +809,88 @@ function u32(value) {
|
|
|
239
809
|
return buffer;
|
|
240
810
|
}
|
|
241
811
|
|
|
242
|
-
function sendJson(response, value) {
|
|
812
|
+
function sendJson(response, value, status = 200) {
|
|
243
813
|
const body = Buffer.from(`${JSON.stringify(value)}\n`);
|
|
244
|
-
response.writeHead(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
814
|
+
response.writeHead(status, {
|
|
815
|
+
'Content-Type': 'application/json',
|
|
816
|
+
'Content-Length': body.length,
|
|
817
|
+
'Cache-Control': 'no-store',
|
|
248
818
|
});
|
|
249
819
|
response.end(body);
|
|
250
820
|
}
|
|
251
821
|
|
|
822
|
+
function errorWithStatus(message, statusCode) {
|
|
823
|
+
const error = new Error(message);
|
|
824
|
+
error.statusCode = statusCode;
|
|
825
|
+
return error;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function errorStatus(error) {
|
|
829
|
+
return Number.isInteger(error.statusCode) ? error.statusCode : 500;
|
|
830
|
+
}
|
|
831
|
+
|
|
252
832
|
function sendText(response, value, status = 200) {
|
|
253
833
|
response.writeHead(status, {
|
|
254
|
-
|
|
255
|
-
|
|
834
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
835
|
+
'Cache-Control': 'no-store',
|
|
256
836
|
});
|
|
257
837
|
response.end(value);
|
|
258
838
|
}
|
|
259
839
|
|
|
260
840
|
function printServerUrls(port, token) {
|
|
261
|
-
const
|
|
262
|
-
const recommended = networkUrls.find(({address}) => isPrivateIPv4(address)) ?? networkUrls[0];
|
|
841
|
+
const {recommended, otherUrls} = serverUrls(port, token);
|
|
263
842
|
if (recommended) {
|
|
264
|
-
console.log(
|
|
265
|
-
console.log(
|
|
843
|
+
console.log('Paste this URL into Await:');
|
|
844
|
+
console.log('');
|
|
266
845
|
console.log(recommended.url);
|
|
267
|
-
console.log(
|
|
846
|
+
console.log('');
|
|
268
847
|
}
|
|
269
|
-
|
|
270
|
-
{url: `http://127.0.0.1:${port}?token=${token}`},
|
|
271
|
-
...networkUrls.filter(item => item !== recommended),
|
|
272
|
-
];
|
|
848
|
+
|
|
273
849
|
if (otherUrls.length > 0) {
|
|
274
|
-
console.log(
|
|
850
|
+
console.log('Other network interfaces, usually not needed:');
|
|
275
851
|
for (const item of otherUrls) {
|
|
276
852
|
console.log(item.url);
|
|
277
853
|
}
|
|
278
854
|
}
|
|
279
855
|
}
|
|
280
856
|
|
|
857
|
+
function writeBridgeInfo(root, port, token) {
|
|
858
|
+
const {recommended, otherUrls} = serverUrls(port, token);
|
|
859
|
+
const info = {
|
|
860
|
+
workspaceRoot: root,
|
|
861
|
+
workspaceName: path.basename(root),
|
|
862
|
+
port,
|
|
863
|
+
token,
|
|
864
|
+
bridgeUrl: `http://127.0.0.1:${port}?token=${token}`,
|
|
865
|
+
appUrl: recommended?.url,
|
|
866
|
+
urls: [recommended, ...otherUrls].filter(Boolean).map(item => item.url),
|
|
867
|
+
updatedAt: new Date().toISOString(),
|
|
868
|
+
};
|
|
869
|
+
fs.mkdirSync(path.dirname(bridgeInfoPath(root)), {recursive: true});
|
|
870
|
+
fs.writeFileSync(bridgeInfoPath(root), `${JSON.stringify(info, null, '\t')}\n`);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function bridgeInfoPath(root) {
|
|
874
|
+
return path.join(root, bridgeInfoDirectory, bridgeInfoFileName);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function serverUrls(port, token) {
|
|
878
|
+
const networkUrls = localNetworkUrls(port, token);
|
|
879
|
+
const recommended = networkUrls.find(({address}) => isPrivateIPv4(address)) ?? networkUrls[0];
|
|
880
|
+
return {
|
|
881
|
+
recommended,
|
|
882
|
+
otherUrls: [
|
|
883
|
+
{url: `http://127.0.0.1:${port}?token=${token}`},
|
|
884
|
+
...networkUrls.filter(item => item !== recommended),
|
|
885
|
+
],
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
281
889
|
function localNetworkUrls(port, token) {
|
|
282
890
|
const urls = [];
|
|
283
891
|
for (const interfaces of Object.values(os.networkInterfaces())) {
|
|
284
892
|
for (const item of interfaces ?? []) {
|
|
285
|
-
if (item.family ===
|
|
893
|
+
if (item.family === 'IPv4' && !item.internal) {
|
|
286
894
|
urls.push({
|
|
287
895
|
address: item.address,
|
|
288
896
|
url: `http://${item.address}:${port}?token=${token}`,
|
|
@@ -290,17 +898,19 @@ function localNetworkUrls(port, token) {
|
|
|
290
898
|
}
|
|
291
899
|
}
|
|
292
900
|
}
|
|
901
|
+
|
|
293
902
|
return urls;
|
|
294
903
|
}
|
|
295
904
|
|
|
296
905
|
function isPrivateIPv4(address) {
|
|
297
|
-
const parts = address.split(
|
|
906
|
+
const parts = address.split('.').map(Number);
|
|
298
907
|
if (parts.length !== 4 || parts.some(part => !Number.isInteger(part))) {
|
|
299
908
|
return false;
|
|
300
909
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
(parts[0] ===
|
|
910
|
+
|
|
911
|
+
return parts[0] === 10
|
|
912
|
+
|| (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31)
|
|
913
|
+
|| (parts[0] === 192 && parts[1] === 168);
|
|
304
914
|
}
|
|
305
915
|
|
|
306
916
|
function urlForRequest(request, token) {
|
|
@@ -308,14 +918,50 @@ function urlForRequest(request, token) {
|
|
|
308
918
|
return `http://${host}?token=${token}`;
|
|
309
919
|
}
|
|
310
920
|
|
|
921
|
+
function isDirectory(value) {
|
|
922
|
+
try {
|
|
923
|
+
return fs.statSync(value).isDirectory();
|
|
924
|
+
} catch {
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
311
929
|
function printHelp() {
|
|
930
|
+
const commandHelp = appCommands
|
|
931
|
+
.map(command => [
|
|
932
|
+
` ${command.usage}`,
|
|
933
|
+
` tool: ${command.name}`,
|
|
934
|
+
` ${command.description}`,
|
|
935
|
+
` inputSchema: ${JSON.stringify(command.inputSchema)}`,
|
|
936
|
+
].join('\n'))
|
|
937
|
+
.join('\n');
|
|
312
938
|
console.log(`Usage:
|
|
313
|
-
await-widget
|
|
939
|
+
await-widget [--port ${defaultPort}]
|
|
940
|
+
await-widget app <command> [options]
|
|
941
|
+
|
|
942
|
+
Start the Await computer connection from a first-level widget folder under a
|
|
943
|
+
package whose package.json includes @await-widget/runtime.
|
|
944
|
+
|
|
945
|
+
What it does:
|
|
946
|
+
- Serves the current widget folder to Await for live sync.
|
|
947
|
+
- Prints a URL to paste into Await's Connect Computer sheet.
|
|
948
|
+
- Writes .await/bridge.json in the package root for app commands.
|
|
949
|
+
- Sends one-shot JSON app commands with await-widget app <command>.
|
|
950
|
+
|
|
951
|
+
Examples:
|
|
952
|
+
npx await-widget
|
|
953
|
+
npx await-widget --port 4344
|
|
954
|
+
npx await-widget app open-syncing-widget-detail
|
|
955
|
+
npx await-widget app wait-for-widget-ready --widget-id 2
|
|
956
|
+
npx await-widget app capture-current-preview --widget-id 2
|
|
314
957
|
|
|
315
|
-
|
|
316
|
-
await
|
|
958
|
+
App command options:
|
|
959
|
+
--workspace <path> Read .await/bridge.json from another package/widget path.
|
|
960
|
+
--bridge-url <url> Send directly to a bridge URL instead of reading bridge.json.
|
|
961
|
+
--json <json> Merge raw JSON arguments into the app command.
|
|
317
962
|
|
|
318
|
-
|
|
963
|
+
App commands:
|
|
964
|
+
${commandHelp}
|
|
319
965
|
`);
|
|
320
966
|
}
|
|
321
967
|
|
package/package.json
CHANGED
package/types/model.d.ts
CHANGED
|
@@ -206,6 +206,12 @@ type AwaitLocationInfo = {
|
|
|
206
206
|
altitudeMeters?: number;
|
|
207
207
|
speedMetersPerSecond?: number;
|
|
208
208
|
courseDegrees?: number;
|
|
209
|
+
country?: string;
|
|
210
|
+
administrativeArea?: string;
|
|
211
|
+
locality?: string;
|
|
212
|
+
subLocality?: string;
|
|
213
|
+
thoroughfare?: string;
|
|
214
|
+
subThoroughfare?: string;
|
|
209
215
|
};
|
|
210
216
|
|
|
211
217
|
type AwaitNowPlayingConfig = {
|