bmweb-cli 0.1.0

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.
Files changed (72) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +351 -0
  3. package/dist/bmweb.js +2790 -0
  4. package/package.json +53 -0
  5. package/runtime/core/bestvm/codec.js +285 -0
  6. package/runtime/core/bestvm/environment.js +116 -0
  7. package/runtime/core/bestvm/executor.js +1483 -0
  8. package/runtime/core/bestvm/index.js +52 -0
  9. package/runtime/core/bestvm/machine.js +491 -0
  10. package/runtime/core/bestvm/operands.js +356 -0
  11. package/runtime/core/bestvm/registers.js +152 -0
  12. package/runtime/core/bestvm/write-guard.js +111 -0
  13. package/runtime/core/ipofile/compile.js +364 -0
  14. package/runtime/core/ipofile/decls.js +187 -0
  15. package/runtime/core/ipofile/emit.js +708 -0
  16. package/runtime/core/ipofile/exec.js +164 -0
  17. package/runtime/core/ipofile/lex.js +243 -0
  18. package/runtime/core/ipofile/parse.js +550 -0
  19. package/runtime/core/ipofile/pool.js +404 -0
  20. package/runtime/core/ipofile/walk.js +431 -0
  21. package/runtime/core/ipovm/builtin-helpers.js +182 -0
  22. package/runtime/core/ipovm/builtins-api.js +610 -0
  23. package/runtime/core/ipovm/builtins-screen.js +493 -0
  24. package/runtime/core/ipovm/builtins-table.js +166 -0
  25. package/runtime/core/ipovm/builtins-text.js +166 -0
  26. package/runtime/core/ipovm/emissions.js +138 -0
  27. package/runtime/core/ipovm/hosts.js +191 -0
  28. package/runtime/core/ipovm/operators.js +229 -0
  29. package/runtime/core/ipovm/structures.js +250 -0
  30. package/runtime/core/ipovm/suspensions.js +241 -0
  31. package/runtime/core/ipovm/tape.js +206 -0
  32. package/runtime/core/ipovm/values.js +241 -0
  33. package/runtime/core/ipovm/vm.js +1166 -0
  34. package/runtime/core/translate.js +526 -0
  35. package/runtime/core/webshim/api-router.js +592 -0
  36. package/runtime/core/webshim/bus.js +95 -0
  37. package/runtime/core/webshim/coding.js +82 -0
  38. package/runtime/core/webshim/data-fetch.js +66 -0
  39. package/runtime/core/webshim/exchange.js +288 -0
  40. package/runtime/core/webshim/framing.js +331 -0
  41. package/runtime/core/webshim/install.js +30 -0
  42. package/runtime/core/webshim/job-runner.js +319 -0
  43. package/runtime/core/webshim/native-bus.js +108 -0
  44. package/runtime/core/webshim/timers.js +82 -0
  45. package/runtime/core/webshim/trace.js +205 -0
  46. package/runtime/core/webshim/transport-base.js +128 -0
  47. package/runtime/core/webshim/variant-resolver.js +249 -0
  48. package/runtime/core/webshim/web-serial-bus.js +734 -0
  49. package/runtime/home/bmweb-home.ips +76 -0
  50. package/runtime/home/bmweb.h +26 -0
  51. package/runtime/screens/activations.js +258 -0
  52. package/runtime/screens/garage/diff.js +331 -0
  53. package/runtime/screens/garage/share.js +276 -0
  54. package/runtime/screens/garage/store.js +547 -0
  55. package/runtime/screens/ipo-runtime/cells.js +176 -0
  56. package/runtime/screens/ipo-runtime/dialogs.js +254 -0
  57. package/runtime/screens/ipo-runtime/home.js +358 -0
  58. package/runtime/screens/ipo-runtime/open.js +393 -0
  59. package/runtime/screens/ipo-runtime/paint-grid.js +106 -0
  60. package/runtime/screens/ipo-runtime/paint-modern.js +424 -0
  61. package/runtime/screens/ipo-runtime/print.js +281 -0
  62. package/runtime/screens/ipo-runtime/program.js +1337 -0
  63. package/runtime/screens/ipo-runtime/protocol.js +464 -0
  64. package/runtime/screens/ipo-runtime/script-scan.js +225 -0
  65. package/runtime/screens/ipo-runtime/translate-sets.js +130 -0
  66. package/runtime/screens/ipo-runtime/ui.js +249 -0
  67. package/runtime/screens/ipo-runtime/wire-policy.js +113 -0
  68. package/runtime/screens/ir.js +324 -0
  69. package/runtime/screens/search/data.js +153 -0
  70. package/runtime/screens/search/match.js +285 -0
  71. package/runtime/screens/search/open.js +66 -0
  72. package/runtime/vendor/fflate.min.js +1 -0
@@ -0,0 +1,276 @@
1
+ /**
2
+ * @file Sharing a stored scan as a link that carries the report itself.
3
+ *
4
+ * Nothing is stored on a server: the compact report is deflated, base64url
5
+ * encoded and put in the URL fragment (#report/<payload>), so the link opens
6
+ * on any copy of the app, hosted or offline, with no car and no account. The
7
+ * VIN is left out of the payload; the car is named by its label and chassis.
8
+ */
9
+
10
+ /* exported garageSharePayload, garageShareEncode, garageShareDecode,
11
+ garageShareUrl, garageShareButton, showGarageSharedReport */
12
+
13
+ /** How long the Share button shows its "Copied" confirmation, ms. */
14
+ const GARAGE_COPY_FLASH_MS = 1600;
15
+
16
+ /** The payload format version, so a later reader can tell an older link. */
17
+ const GARAGE_SHARE_V = 1;
18
+
19
+ /**
20
+ * What a shared link carries: the report and enough to caption it. The VIN
21
+ * is deliberately absent, as it is in the beta reports.
22
+ * @param {GarageScan} scan - the stored scan
23
+ * @param {GarageCar} [car] - the car it belongs to, for its label
24
+ * @returns {{v: number, kind: string, at: string, chassis: string, label: string, report: object, summary: object}}
25
+ */
26
+ function garageSharePayload(scan, car) {
27
+ const label =
28
+ typeof garageCarLabel === 'function' && car
29
+ ? garageCarLabel(car)
30
+ : (car && car.label) || '';
31
+ return {
32
+ v: GARAGE_SHARE_V,
33
+ kind: scan.kind,
34
+ at: scan.at,
35
+ chassis: scan.chassis || (car && car.chassis) || '',
36
+ label,
37
+ report: scan.report,
38
+ summary: scan.summary,
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Base64url of bytes (no padding, URL-safe alphabet).
44
+ * @param {Uint8Array} bytes - the bytes
45
+ * @returns {string}
46
+ */
47
+ function garageB64urlEncode(bytes) {
48
+ let bin = '';
49
+ for (let i = 0; i < bytes.length; i += 0x8000)
50
+ bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
51
+ return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
52
+ }
53
+
54
+ /**
55
+ * Bytes of a base64url string.
56
+ * @param {string} s - the text
57
+ * @returns {Uint8Array}
58
+ */
59
+ function garageB64urlDecode(s) {
60
+ const b64 = String(s).replace(/-/g, '+').replace(/_/g, '/');
61
+ const bin = atob(b64 + '='.repeat((4 - (b64.length % 4)) % 4));
62
+ const out = new Uint8Array(bin.length);
63
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
64
+ return out;
65
+ }
66
+
67
+ /**
68
+ * Run bytes through a CompressionStream / DecompressionStream.
69
+ * @param {Uint8Array} bytes - the input
70
+ * @param {CompressionStream|DecompressionStream} stream - the transform
71
+ * @returns {Promise<Uint8Array>}
72
+ */
73
+ async function garagePipe(bytes, stream) {
74
+ const res = new Response(new Blob([bytes]).stream().pipeThrough(stream));
75
+ return new Uint8Array(await res.arrayBuffer());
76
+ }
77
+
78
+ /**
79
+ * Encode a scan as the link payload: JSON, deflate-raw, base64url. A
80
+ * whole-car read compresses to a few KB, well inside what a browser and a
81
+ * forum post carry in a URL.
82
+ * @param {GarageScan} scan - the stored scan
83
+ * @param {GarageCar} [car] - the car it belongs to
84
+ * @returns {Promise<string>} the payload
85
+ */
86
+ async function garageShareEncode(scan, car) {
87
+ const json = JSON.stringify(garageSharePayload(scan, car));
88
+ const raw = new TextEncoder().encode(json);
89
+ const packed = await garagePipe(raw, new CompressionStream('deflate-raw'));
90
+ return garageB64urlEncode(packed);
91
+ }
92
+
93
+ /**
94
+ * Decode a link payload back into what garageSharePayload built.
95
+ * @param {string} payload - the base64url text from the URL
96
+ * @returns {Promise<object|null>} the payload object, or null when unreadable
97
+ */
98
+ async function garageShareDecode(payload) {
99
+ try {
100
+ const packed = garageB64urlDecode(payload);
101
+ const raw = await garagePipe(
102
+ packed,
103
+ new DecompressionStream('deflate-raw')
104
+ );
105
+ const obj = JSON.parse(new TextDecoder().decode(raw));
106
+ if (!obj || typeof obj !== 'object' || !obj.report) return null;
107
+ return obj;
108
+ } catch (e) {
109
+ return null;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * The shareable URL for a scan: this app's own address with the payload in
115
+ * the fragment, so it opens on the hosted site or an offline copy alike.
116
+ * @param {GarageScan} scan - the stored scan
117
+ * @param {GarageCar} [car] - the car it belongs to
118
+ * @param {string} [base] - the page address without a fragment (defaults to this page)
119
+ * @returns {Promise<string>}
120
+ */
121
+ async function garageShareUrl(scan, car, base) {
122
+ const payload = await garageShareEncode(scan, car);
123
+ const here =
124
+ base != null
125
+ ? base
126
+ : typeof location !== 'undefined'
127
+ ? location.href.split('#')[0]
128
+ : '';
129
+ return `${here}#report/${payload}`;
130
+ }
131
+
132
+ /**
133
+ * Copy text to the clipboard, through the wiring app's helper when it is
134
+ * loaded (async clipboard with a textarea fallback for file://).
135
+ * @param {string} text - what to copy
136
+ * @returns {Promise<boolean>}
137
+ */
138
+ async function garageCopyText(text) {
139
+ if (typeof wiringCopyText === 'function') return wiringCopyText(text);
140
+ try {
141
+ await navigator.clipboard.writeText(text);
142
+ return true;
143
+ } catch (e) {
144
+ return false;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * A Share button for a stored scan: copies the report link and flashes
150
+ * "Copied", exactly like the wiring app's Share.
151
+ * @param {GarageScan} scan - the stored scan
152
+ * @param {GarageCar} [car] - the car it belongs to
153
+ * @returns {HTMLButtonElement}
154
+ */
155
+ function garageShareButton(scan, car) {
156
+ const btn = document.createElement('button');
157
+ btn.type = 'button';
158
+ btn.className = 'btn garage-share';
159
+ btn.textContent = 'Share';
160
+ btn.title = 'Copy a link that carries this report';
161
+ btn.onclick = async () => {
162
+ const url = await garageShareUrl(scan, car);
163
+ const ok = await garageCopyText(url);
164
+ const original = 'Share';
165
+ btn.textContent = ok ? '✓ Copied' : 'Copy failed';
166
+ btn.classList.toggle('copied', ok);
167
+ clearTimeout(btn._copyTimer);
168
+ btn._copyTimer = setTimeout(() => {
169
+ btn.textContent = original;
170
+ btn.classList.remove('copied');
171
+ }, GARAGE_COPY_FLASH_MS);
172
+ };
173
+ return btn;
174
+ }
175
+
176
+ /**
177
+ * A report someone sent as a link: drawn by the live view's own renderer,
178
+ * with Save to garage so the recipient can keep it against a car of theirs.
179
+ * @param {string} payload - the base64url payload from #report/<payload>
180
+ * @returns {Promise<void>}
181
+ */
182
+ async function showGarageSharedReport(payload) {
183
+ const p = await garageShareDecode(payload);
184
+ lastScreen = () => showGarageSharedReport(payload);
185
+ setCrumbs([
186
+ { label: 'Vehicles', fn: showChassis },
187
+ { label: 'Garage', fn: showGarage },
188
+ { label: 'Shared report' },
189
+ ]);
190
+ document.body.classList.add('apps-section');
191
+ sbLeft.textContent = 'shared report';
192
+ if (!p) {
193
+ view.innerHTML = head('Garage', 'Shared report', '');
194
+ view.appendChild(
195
+ errorBlock(
196
+ 'This link does not carry a readable report. It may have been cut short when it was pasted.'
197
+ )
198
+ );
199
+ setActions([garageBackAction(showGarage)]);
200
+ return;
201
+ }
202
+ const kindText = p.kind === 'ident' ? 'Identification' : 'Fault memories';
203
+ const who = [p.label, p.chassis && dispChassis(p.chassis)]
204
+ .filter(Boolean)
205
+ .join(' · ');
206
+ view.innerHTML = head(
207
+ 'Garage',
208
+ 'Shared report',
209
+ `${who ? `${who} · ` : ''}${kindText} · ${garageDateText(p.at)}`
210
+ );
211
+ const scan = {
212
+ kind: p.kind,
213
+ at: p.at,
214
+ chassis: p.chassis,
215
+ report: p.report,
216
+ summary: p.summary || garageScanSummary(p.report),
217
+ };
218
+ sbRight.textContent = garageScanCountsText(scan);
219
+ const body = document.createElement('div');
220
+ view.appendChild(body);
221
+ if (typeof ipoProtocolRender === 'function')
222
+ await ipoProtocolRender(body, garageViewFor(scan));
223
+ garageDisarmStoredReport(body);
224
+ if (typeof garageAttachEnv === 'function')
225
+ await garageAttachEnv(body, scan.report, {
226
+ screensFor:
227
+ typeof garageScreensFor === 'function' ? garageScreensFor : null,
228
+ });
229
+ // keep it: the report goes into the Garage against a car the recipient picks
230
+ const keep = async () => {
231
+ if (typeof garagePickCar !== 'function') return;
232
+ const car = await garagePickCar({ vin: '', chassis: p.chassis });
233
+ if (!car) return;
234
+ const kept = garageAddScan(
235
+ car.id,
236
+ { report: p.report },
237
+ {
238
+ chassis: p.chassis,
239
+ at: p.at,
240
+ }
241
+ );
242
+ if (kept) showGarageScan(car.id, kept.id);
243
+ };
244
+ const bar = body.querySelector('.quick-bar-btns');
245
+ if (bar) {
246
+ const save = document.createElement('button');
247
+ save.className = 'btn garage-save';
248
+ save.textContent = 'Save to garage';
249
+ save.onclick = keep;
250
+ bar.appendChild(save);
251
+ }
252
+ setActions([
253
+ { key: '1', label: 'Save to garage', fn: keep },
254
+ {
255
+ key: 'p',
256
+ label: 'Print',
257
+ kind: 'print',
258
+ fn: () =>
259
+ garagePrintScan(
260
+ { label: p.label || 'Shared report', chassis: p.chassis },
261
+ scan
262
+ ),
263
+ },
264
+ garageBackAction(showGarage),
265
+ ]);
266
+ }
267
+
268
+ // node loads these pieces as modules; the browser gives them one shared scope
269
+ if (typeof module !== 'undefined' && module.exports) {
270
+ module.exports = {
271
+ garageSharePayload,
272
+ garageShareEncode,
273
+ garageShareDecode,
274
+ garageShareUrl,
275
+ };
276
+ }