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
package/dist/bmweb.js ADDED
@@ -0,0 +1,2790 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/args.ts
4
+ var CliError = class extends Error {
5
+ };
6
+ function parseArgs(argv, spec) {
7
+ const positional = [];
8
+ const flags = {};
9
+ const byAlias = /* @__PURE__ */ new Map();
10
+ for (const [name, s] of Object.entries(spec))
11
+ if (s.alias) byAlias.set(s.alias, name);
12
+ const set = (name, raw) => {
13
+ const s = spec[name];
14
+ if (!s) throw new CliError(`unknown option --${name}`);
15
+ if (s.kind === "bool") {
16
+ if (raw !== null) throw new CliError(`--${name} takes no value`);
17
+ flags[name] = true;
18
+ return;
19
+ }
20
+ if (raw === null) throw new CliError(`--${name} needs a value`);
21
+ if (s.kind === "number") {
22
+ const n = Number(raw);
23
+ if (!Number.isFinite(n)) throw new CliError(`--${name} needs a number`);
24
+ flags[name] = n;
25
+ return;
26
+ }
27
+ if (s.kind === "list") {
28
+ const list = flags[name] || [];
29
+ list.push(raw);
30
+ flags[name] = list;
31
+ return;
32
+ }
33
+ flags[name] = raw;
34
+ };
35
+ for (let i = 0; i < argv.length; i++) {
36
+ const a = argv[i];
37
+ if (a === "--") {
38
+ positional.push(...argv.slice(i + 1));
39
+ break;
40
+ }
41
+ if (a.startsWith("--")) {
42
+ const eq = a.indexOf("=");
43
+ const name = eq >= 0 ? a.slice(2, eq) : a.slice(2);
44
+ const s = spec[name];
45
+ if (!s) throw new CliError(`unknown option --${name}`);
46
+ if (eq >= 0) set(name, a.slice(eq + 1));
47
+ else if (s.kind === "bool") set(name, null);
48
+ else {
49
+ const v = argv[i + 1];
50
+ if (v === void 0) throw new CliError(`--${name} needs a value`);
51
+ set(name, v);
52
+ i++;
53
+ }
54
+ continue;
55
+ }
56
+ if (a.length >= 2 && a[0] === "-" && a !== "-") {
57
+ const name = byAlias.get(a[1]);
58
+ if (!name) throw new CliError(`unknown option ${a.slice(0, 2)}`);
59
+ const s = spec[name];
60
+ if (a.length > 2) set(name, a.slice(2));
61
+ else if (s.kind === "bool") set(name, null);
62
+ else {
63
+ const v = argv[i + 1];
64
+ if (v === void 0) throw new CliError(`${a} needs a value`);
65
+ set(name, v);
66
+ i++;
67
+ }
68
+ continue;
69
+ }
70
+ positional.push(a);
71
+ }
72
+ return { positional, flags };
73
+ }
74
+ function helpLines(spec) {
75
+ const rows = Object.entries(spec).map(([name, s]) => {
76
+ const value = s.kind === "bool" ? "" : s.kind === "number" ? " N" : " <value>";
77
+ const left = `${s.alias ? `-${s.alias}, ` : " "}--${name}${value}`;
78
+ return [left, s.help || ""];
79
+ });
80
+ const w = Math.max(0, ...rows.map((r) => r[0].length));
81
+ return rows.map(([l, h]) => ` ${l.padEnd(w)} ${h}`.trimEnd());
82
+ }
83
+
84
+ // src/ipo.ts
85
+ import { readdirSync, readFileSync as readFileSync3, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
86
+ import { basename, dirname as dirname2, join as join4, resolve } from "node:path";
87
+
88
+ // src/table.ts
89
+ function formatTable(rows, header, indent = "") {
90
+ const all = header ? [header, ...rows] : rows;
91
+ const text = all.map((r) => r.map((c) => c === void 0 ? "" : String(c)));
92
+ const cols = Math.max(0, ...text.map((r) => r.length));
93
+ const widths = [];
94
+ for (let c = 0; c < cols; c++)
95
+ widths[c] = Math.max(0, ...text.map((r) => (r[c] || "").length));
96
+ const line = (r) => indent + r.map(
97
+ (cell, c) => c === cols - 1 ? cell : cell.padEnd(widths[c])
98
+ ).join(" ").trimEnd();
99
+ const out = text.map(line);
100
+ if (header) {
101
+ const rule = widths.map((w) => "-".repeat(w));
102
+ out.splice(1, 0, line(rule));
103
+ }
104
+ return out;
105
+ }
106
+ function formatCount(n) {
107
+ return n.toLocaleString("en-US");
108
+ }
109
+
110
+ // src/runtime.ts
111
+ import { readFileSync as readFileSync2 } from "node:fs";
112
+ import { join as join3 } from "node:path";
113
+ import { fileURLToPath } from "node:url";
114
+ import { createContext, runInContext } from "node:vm";
115
+
116
+ // src/runtime-files.json
117
+ var runtime_files_default = [
118
+ "vendor/fflate.min.js",
119
+ "core/webshim/timers.js",
120
+ "core/webshim/trace.js",
121
+ "core/webshim/framing.js",
122
+ "core/webshim/exchange.js",
123
+ "core/webshim/transport-base.js",
124
+ "core/webshim/native-bus.js",
125
+ "core/webshim/web-serial-bus.js",
126
+ "core/webshim/bus.js",
127
+ "core/webshim/data-fetch.js",
128
+ "core/webshim/job-runner.js",
129
+ "core/webshim/variant-resolver.js",
130
+ "core/webshim/coding.js",
131
+ "core/webshim/api-router.js",
132
+ "core/webshim/install.js",
133
+ "core/bestvm/write-guard.js",
134
+ "core/bestvm/codec.js",
135
+ "core/bestvm/machine.js",
136
+ "core/bestvm/registers.js",
137
+ "core/bestvm/operands.js",
138
+ "core/bestvm/environment.js",
139
+ "core/bestvm/executor.js",
140
+ "core/bestvm/index.js",
141
+ "core/ipovm/values.js",
142
+ "core/ipovm/operators.js",
143
+ "core/ipovm/hosts.js",
144
+ "core/ipovm/emissions.js",
145
+ "core/ipovm/tape.js",
146
+ "core/ipovm/builtin-helpers.js",
147
+ "core/ipovm/builtins-screen.js",
148
+ "core/ipovm/builtins-api.js",
149
+ "core/ipovm/builtins-text.js",
150
+ "core/ipovm/builtins-table.js",
151
+ "core/ipovm/structures.js",
152
+ "core/ipovm/suspensions.js",
153
+ "core/ipovm/vm.js",
154
+ "core/ipofile/pool.js",
155
+ "core/ipofile/decls.js",
156
+ "core/ipofile/walk.js",
157
+ "core/ipofile/exec.js",
158
+ "core/ipofile/lex.js",
159
+ "core/ipofile/parse.js",
160
+ "core/ipofile/emit.js",
161
+ "core/ipofile/compile.js",
162
+ "core/translate.js",
163
+ "screens/ir.js",
164
+ "screens/ipo-runtime/script-scan.js",
165
+ "screens/ipo-runtime/translate-sets.js",
166
+ "screens/ipo-runtime/wire-policy.js",
167
+ "screens/ipo-runtime/program.js",
168
+ "screens/ipo-runtime/cells.js",
169
+ "screens/ipo-runtime/paint-modern.js",
170
+ "screens/ipo-runtime/paint-grid.js",
171
+ "screens/ipo-runtime/dialogs.js",
172
+ "screens/ipo-runtime/print.js",
173
+ "screens/ipo-runtime/protocol.js",
174
+ "screens/ipo-runtime/ui.js",
175
+ "screens/ipo-runtime/open.js",
176
+ "screens/ipo-runtime/home.js",
177
+ "home/bmweb-home.ips",
178
+ "home/bmweb.h",
179
+ "screens/activations.js",
180
+ "screens/garage/store.js",
181
+ "screens/garage/diff.js",
182
+ "screens/garage/share.js",
183
+ "screens/search/data.js",
184
+ "screens/search/match.js",
185
+ "screens/search/open.js"
186
+ ];
187
+
188
+ // src/site.ts
189
+ import {
190
+ existsSync,
191
+ mkdirSync,
192
+ readFileSync,
193
+ statSync,
194
+ writeFileSync
195
+ } from "node:fs";
196
+ import { dirname, join as join2 } from "node:path";
197
+
198
+ // src/cache.ts
199
+ import { homedir } from "node:os";
200
+ import { join } from "node:path";
201
+ var CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
202
+ function cacheDir(env = process.env) {
203
+ const base = env.XDG_CACHE_HOME && env.XDG_CACHE_HOME.trim() ? env.XDG_CACHE_HOME : join(homedir(), ".cache");
204
+ return join(base, "bmweb-cli");
205
+ }
206
+ function indexCachePath(env = process.env) {
207
+ return join(cacheDir(env), "search-index.json.gz");
208
+ }
209
+
210
+ // src/site.ts
211
+ var DEFAULT_API = "https://bmweb.danner.ink/";
212
+ var config = {
213
+ base: DEFAULT_API,
214
+ refresh: false,
215
+ env: process.env,
216
+ fetchImpl: (...a) => fetch(...a)
217
+ };
218
+ function configureSite(opts) {
219
+ if (opts.base !== void 0) config.base = normalizeBase(opts.base);
220
+ if (opts.refresh !== void 0) config.refresh = opts.refresh;
221
+ if (opts.env !== void 0) config.env = opts.env;
222
+ if (opts.fetchImpl !== void 0) config.fetchImpl = opts.fetchImpl;
223
+ }
224
+ function normalizeBase(base) {
225
+ const s = String(base || "").trim();
226
+ return s.endsWith("/") ? s : `${s}/`;
227
+ }
228
+ function sitePath(input) {
229
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
230
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(url)) return null;
231
+ return url.replace(/^\/+/, "");
232
+ }
233
+ function siteCachePath(rel, env = config.env) {
234
+ const clean = rel.split("?")[0];
235
+ return join2(cacheDir(env), "site", ...clean.split("/"));
236
+ }
237
+ async function siteGet(rel) {
238
+ const file = siteCachePath(rel);
239
+ const have = existsSync(file);
240
+ const fresh = have && Date.now() - statSync(file).mtimeMs < CACHE_MAX_AGE_MS;
241
+ if (have && fresh && !config.refresh)
242
+ return { status: 200, bytes: readFileSync(file), fromCache: true };
243
+ let status = 0;
244
+ let bytes = null;
245
+ try {
246
+ const r = await config.fetchImpl(`${config.base}${rel}`);
247
+ status = r.status;
248
+ if (r.ok) bytes = new Uint8Array(await r.arrayBuffer());
249
+ } catch {
250
+ status = 0;
251
+ }
252
+ if (bytes) {
253
+ mkdirSync(dirname(file), { recursive: true });
254
+ writeFileSync(file, bytes);
255
+ return { status: 200, bytes, fromCache: false };
256
+ }
257
+ if (have && status !== 404)
258
+ return { status: 200, bytes: readFileSync(file), fromCache: true };
259
+ return { status: status || 503, bytes: null, fromCache: false };
260
+ }
261
+ async function siteFetch(input, init) {
262
+ const rel = sitePath(input);
263
+ if (rel === null) return config.fetchImpl(input, init);
264
+ const r = await siteGet(rel);
265
+ if (!r.bytes)
266
+ return new Response(JSON.stringify({ error: `${rel}: HTTP ${r.status}` }), {
267
+ status: r.status,
268
+ statusText: r.status === 404 ? "Not Found" : "Unavailable"
269
+ });
270
+ return new Response(r.bytes, { status: 200 });
271
+ }
272
+
273
+ // src/runtime.ts
274
+ var RUNTIME_DIR = fileURLToPath(
275
+ new URL("../runtime/", import.meta.url)
276
+ );
277
+ var loaded = null;
278
+ var sandboxRef = null;
279
+ var apiImpl = null;
280
+ function setApiImpl(fn) {
281
+ apiImpl = fn;
282
+ }
283
+ function runtimeGlobals() {
284
+ loadRuntime();
285
+ return sandboxRef;
286
+ }
287
+ function hostGlobals() {
288
+ const noop = () => {
289
+ };
290
+ const sandbox = {
291
+ // the scripts test `typeof window` and read window.<x>; the context's own
292
+ // global stands in, as it does in the page (self: fflate's UMD attaches
293
+ // its global to `self` when there is no CommonJS `exports`)
294
+ console,
295
+ // timers.js's bmwSleep falls back to setTimeout where there is no
296
+ // Worker; program.js schedules screen cycles and drains key presses
297
+ // through setTimeout; activations.js defers a session end a microtask
298
+ setTimeout,
299
+ clearTimeout,
300
+ setInterval,
301
+ clearInterval,
302
+ queueMicrotask,
303
+ // share.js: TextEncoder/TextDecoder, Blob, Response and the two
304
+ // compression streams encode and decode the link payload; btoa/atob do
305
+ // the base64url. api-router.js builds Responses and reads query strings;
306
+ // data-fetch.js decodes archive members. All are Node 18+ globals,
307
+ // handed into the fresh realm.
308
+ TextEncoder,
309
+ TextDecoder,
310
+ Blob,
311
+ Response,
312
+ Request,
313
+ URL,
314
+ URLSearchParams,
315
+ CompressionStream,
316
+ DecompressionStream,
317
+ btoa,
318
+ atob,
319
+ // WHAT THE APP READS ITS DATA THROUGH. install.js takes window.fetch as
320
+ // the real fetch and wraps it with the shim; every site-relative path
321
+ // the shim then asks for is served from the hosted site through the
322
+ // disk cache (site.ts), never from a local vendor tree.
323
+ fetch: siteFetch,
324
+ // web-serial-bus.js asks navigator.serial for the port; the CLI fills
325
+ // this in with the Node port once one is chosen (live.ts)
326
+ navigator: { serial: void 0 },
327
+ // activations.js registers a pagehide hook at load; the bus announces
328
+ // cable events; neither has a page here
329
+ addEventListener: noop,
330
+ removeEventListener: noop,
331
+ dispatchEvent: noop,
332
+ // ui.js writes the key caption into the document's style at render
333
+ // time and the harness stubs getElementById; neither draws anything
334
+ document: {
335
+ getElementById: () => null,
336
+ documentElement: { style: { setProperty: noop, removeProperty: noop } }
337
+ },
338
+ // the status bar the runtime writes to
339
+ sbLeft: { textContent: "" },
340
+ sbRight: { textContent: "" },
341
+ // HTML escaping, identity here: no DOM receives the text
342
+ esc: (s) => String(s == null ? "" : s),
343
+ // the F-key bar; the CLI's own UI adapter renders keys itself
344
+ setActions: noop,
345
+ // translate.js and translate-sets.js gate on the language setting
346
+ lang: () => "en",
347
+ // the fault dictionaries the fed results are translated through: the
348
+ // large generated ones are not in this package, so results stay as the
349
+ // module sent them (the app behaves the same in a build without them)
350
+ BMW_ENV_TEXT: {},
351
+ BMW_FAULT_PHRASES: {},
352
+ BMW_FAULT_DB: {},
353
+ scopedFaultDb: () => null,
354
+ // a chassis id's display name (core/core/ui.js); the id is fine here
355
+ dispChassis: (id) => String(id || ""),
356
+ // store.js persists through Settings and wire-policy.js reads the
357
+ // confirmActuators preference; the CLI stores nothing and confirms
358
+ // every actuator drive, so a read-only stand-in returning the default
359
+ // is exact
360
+ Settings: {
361
+ get: (_k, d) => d,
362
+ set: noop
363
+ },
364
+ // open.js's searchOpenHit assigns location.hash; api-router.js derives
365
+ // WEB_BASE from the pathname (empty: paths hang off the site root)
366
+ location: { hash: "", href: "", pathname: "/", search: "" },
367
+ // the engine client (core/core/api.js): a route in, its JSON out, the
368
+ // server's `error` field as the thrown message. Routed through apiImpl
369
+ // so a test can put a fake car behind the very same call.
370
+ api: async (path, opts) => {
371
+ if (apiImpl) return apiImpl(path, opts);
372
+ const f = sandbox.fetch;
373
+ const res = await f(path, opts);
374
+ if (!res.ok) {
375
+ const body = await res.json().catch(() => ({}));
376
+ throw new Error(body.error || res.statusText);
377
+ }
378
+ return res.json();
379
+ },
380
+ // the result-set projection (core/core/api.js): the engine's synthetic
381
+ // system set dropped when it leads
382
+ dataSets: (sets) => {
383
+ const list = sets || [];
384
+ const s = list[0];
385
+ const system = !!s && typeof s === "object" && ("SAETZE" in s || "JOBNAME" in s || "OBJECT" in s);
386
+ return list.length && system ? list.slice(1) : list;
387
+ }
388
+ };
389
+ sandbox.window = sandbox;
390
+ sandbox.self = sandbox;
391
+ sandbox.globalThis = sandbox;
392
+ return sandbox;
393
+ }
394
+ function loadRuntime() {
395
+ if (loaded) return loaded;
396
+ const sandbox = hostGlobals();
397
+ const collected = {};
398
+ sandbox.module = {
399
+ get exports() {
400
+ return collected;
401
+ },
402
+ set exports(v) {
403
+ Object.assign(collected, v);
404
+ }
405
+ };
406
+ const context = createContext(sandbox);
407
+ for (const rel of runtime_files_default) {
408
+ if (!rel.endsWith(".js")) continue;
409
+ const file = join3(RUNTIME_DIR, rel);
410
+ let code;
411
+ try {
412
+ code = readFileSync2(file, "utf8");
413
+ } catch {
414
+ throw new Error(
415
+ `runtime file missing: ${rel} (the package was built without scripts/sync-runtime.mjs)`
416
+ );
417
+ }
418
+ runInContext(code, context, { filename: rel });
419
+ }
420
+ for (const name of LIFTED_GLOBALS) {
421
+ collected[name] = runInContext(
422
+ `typeof ${name} === 'undefined' ? undefined : ${name}`,
423
+ context
424
+ );
425
+ }
426
+ sandboxRef = sandbox;
427
+ loaded = collected;
428
+ return loaded;
429
+ }
430
+ var LIFTED_GLOBALS = [
431
+ "IPO_IDENT_ROWS",
432
+ "IPO_TICK_MS",
433
+ "webBus",
434
+ "withBusLock",
435
+ "webResolveVariant",
436
+ "webResolveVariantLast",
437
+ "isWriteJob",
438
+ "ipoProtocolReport",
439
+ // home.js's host object: the status builtin reads it by name, so the
440
+ // CLI hangs its own status on it rather than on the VM
441
+ "IPO_HOME_HOST"
442
+ ];
443
+
444
+ // src/ipo.ts
445
+ var SHIFT_BASE = 10;
446
+ var JOB_NAME_RE = /^[A-Z][A-Z0-9_]{2,}$/;
447
+ var JOB_CALL_RE = /^(INP.?apiJob|INPAapiFsLesen|ApiJobFsLesenFAB)$/;
448
+ var ACTION_CALLS = /* @__PURE__ */ new Set([
449
+ "exit",
450
+ "printscreen",
451
+ "printfile",
452
+ "select",
453
+ "deselect",
454
+ "start",
455
+ "stop",
456
+ "control",
457
+ "messagebox"
458
+ ]);
459
+ var MAX_CALL_DEPTH = 4;
460
+ function keyName(nr) {
461
+ return nr > SHIFT_BASE ? `Shift+F${nr - SHIFT_BASE}` : `F${nr}`;
462
+ }
463
+ function scanBody(exec, toks, start, end, R, depth = 0, seen = /* @__PURE__ */ new Set()) {
464
+ const out = {
465
+ jobs: [],
466
+ screen: null,
467
+ frequent: false,
468
+ submenu: null,
469
+ scriptchange: null,
470
+ actions: [],
471
+ title: null
472
+ };
473
+ const byid = exec.byid || {};
474
+ let args = [];
475
+ const addJob = (j) => {
476
+ if (!out.jobs.includes(j)) out.jobs.push(j);
477
+ };
478
+ const strings = () => args.filter((t) => t.op === "const" && t.t === "s").map((t) => String(t.v));
479
+ const merge = (inner) => {
480
+ inner.jobs.forEach(addJob);
481
+ if (!out.screen && inner.screen) {
482
+ out.screen = inner.screen;
483
+ out.frequent = inner.frequent;
484
+ }
485
+ if (!out.submenu && inner.submenu) out.submenu = inner.submenu;
486
+ if (!out.scriptchange && inner.scriptchange)
487
+ out.scriptchange = inner.scriptchange;
488
+ if (!out.title && inner.title) out.title = inner.title;
489
+ for (const a of inner.actions)
490
+ if (!out.actions.includes(a)) out.actions.push(a);
491
+ };
492
+ for (let i = start; i < end && i < toks.length; i++) {
493
+ const t = toks[i];
494
+ if (t.op === "frame") {
495
+ args = [];
496
+ continue;
497
+ }
498
+ if (t.op === "const" || t.op === "var" || t.op === "procref") {
499
+ args.push(t);
500
+ continue;
501
+ }
502
+ if (t.op === "calluser") {
503
+ const name2 = byid[`func:${t.n}`];
504
+ if (name2 && depth < MAX_CALL_DEPTH && !seen.has(name2)) {
505
+ const body = exec.procs[name2];
506
+ if (body) {
507
+ const next = new Set(seen);
508
+ next.add(name2);
509
+ merge(scanBody(exec, body, 0, body.length, R, depth + 1, next));
510
+ }
511
+ }
512
+ args = [];
513
+ continue;
514
+ }
515
+ if (t.op !== "call") continue;
516
+ const name = t.name || "";
517
+ if (JOB_CALL_RE.test(name)) {
518
+ const s = strings();
519
+ let job;
520
+ if (/^INP.?apiJob$/.test(name)) {
521
+ const second = args[1];
522
+ if (second && second.op === "const" && second.t === "s")
523
+ job = String(second.v);
524
+ }
525
+ if (job === void 0) {
526
+ const first = args[0];
527
+ const sgbd = first && first.op === "const" && first.t === "s" ? String(first.v) : "";
528
+ job = s.find((v) => v !== sgbd && JOB_NAME_RE.test(v));
529
+ }
530
+ if (job && JOB_NAME_RE.test(job)) addJob(job);
531
+ } else if (name === "setscreen") {
532
+ const ref = args.find(
533
+ (a) => a.op === "procref" && a.kind === R.IPO_REF_SCREEN
534
+ );
535
+ const flag = args.find(
536
+ (a) => a.op === "const" && (a.t === "b" || a.t === "i")
537
+ );
538
+ const scr = ref ? byid[`screen:${ref.n}`] : void 0;
539
+ if (scr && !out.screen) {
540
+ out.screen = scr;
541
+ out.frequent = !!(flag && flag.v);
542
+ }
543
+ } else if (name === "setmenu") {
544
+ const ref = args.find(
545
+ (a) => a.op === "procref" && a.kind === R.IPO_REF_MENU
546
+ );
547
+ const menu = ref ? byid[`menu:${ref.n}`] : void 0;
548
+ if (menu && !out.submenu) out.submenu = menu;
549
+ } else if (name === "scriptchange") {
550
+ const s = strings();
551
+ if (s[0] && !out.scriptchange) out.scriptchange = s[0];
552
+ } else if (name === "setmenutitle" || name === "settitle") {
553
+ const s = strings();
554
+ if (s[0] && !out.title) out.title = s[0];
555
+ } else if (ACTION_CALLS.has(name)) {
556
+ if (!out.actions.includes(name)) out.actions.push(name);
557
+ }
558
+ args = [];
559
+ }
560
+ return out;
561
+ }
562
+ function scanScreen(exec, screen, R) {
563
+ const toks = screen ? exec.procs[screen] : void 0;
564
+ if (!toks) return null;
565
+ return scanBody(exec, toks, 0, toks.length, R);
566
+ }
567
+ function describeKey(exec, menu, item, R) {
568
+ const toks = exec.procs[menu] || [];
569
+ const own = scanBody(exec, toks, item.start, item.end, R);
570
+ const scr = scanScreen(exec, own.screen, R);
571
+ const jobs = [...own.jobs];
572
+ for (const j of scr ? scr.jobs : []) if (!jobs.includes(j)) jobs.push(j);
573
+ return {
574
+ menu,
575
+ nr: item.nr,
576
+ key: keyName(item.nr),
577
+ label: item.label,
578
+ hidden: item.hidden,
579
+ screen: own.screen,
580
+ screenTitle: scr ? scr.title : null,
581
+ frequent: own.frequent,
582
+ submenu: own.submenu,
583
+ scriptchange: own.scriptchange,
584
+ ownJobs: own.jobs,
585
+ screenJobs: scr ? scr.jobs : [],
586
+ jobs,
587
+ // the app's own verdict: the write classifier, minus the session
588
+ // plumbing every script sends without asking (wire-policy.js)
589
+ writes: jobs.filter((j) => R.ipoNeedsConfirm(j)),
590
+ actions: own.actions
591
+ };
592
+ }
593
+ function describeMenu(exec, name, R) {
594
+ const toks = exec.procs[name] || [];
595
+ const items = R.ipoMenuItems(exec, name);
596
+ const firstItem = items.length ? items[0].start - 1 : toks.length;
597
+ const prologue = scanBody(exec, toks, 0, firstItem, R);
598
+ const shown = R.ipoScreenForMenu(exec, name);
599
+ return {
600
+ name,
601
+ title: prologue.title,
602
+ screen: shown ? shown.screen : prologue.screen,
603
+ keys: items.map((it) => describeKey(exec, name, it, R))
604
+ };
605
+ }
606
+ function menuNames(exec) {
607
+ return Object.entries(exec.byid || {}).filter(([k]) => k.startsWith("menu:")).sort((a, b) => Number(a[0].split(":")[1]) - Number(b[0].split(":")[1])).map(([, v]) => v);
608
+ }
609
+ function describeScript(exec, source, R, includes = Array.from(exec.includes || [])) {
610
+ const inv = R.ipofInventory(exec);
611
+ const entry = exec.procs.inpainit ? "inpainit" : exec.procs.SgbdInpaCheck ? "SgbdInpaCheck" : null;
612
+ const entryScan = entry ? scanBody(exec, exec.procs[entry], 0, Infinity, R) : null;
613
+ return {
614
+ name: exec.ecu,
615
+ source,
616
+ form: exec.coding ? "NCS coding dispatcher (drives the coding host, not a diagnostic session)" : "INPA diagnostic script",
617
+ bytes: exec.bytes ?? null,
618
+ unknown: exec.unknown ?? 0,
619
+ entry,
620
+ rootMenu: entryScan ? entryScan.submenu : null,
621
+ rootScreen: entryScan ? entryScan.screen : null,
622
+ includes,
623
+ imports: Object.values(exec.imports || {}),
624
+ procedures: Object.keys(exec.procs).length,
625
+ functions: inv.funcs.length,
626
+ machines: inv.machines.length,
627
+ screens: inv.screens.length,
628
+ menus: menuNames(exec).map((m) => describeMenu(exec, m, R))
629
+ };
630
+ }
631
+ function gatherIncludes(dirs, skip) {
632
+ const R = loadRuntime();
633
+ const files = {};
634
+ const own = resolve(skip);
635
+ for (const dir of dirs) {
636
+ let names;
637
+ try {
638
+ names = readdirSync(dir);
639
+ } catch {
640
+ throw new CliError(`include directory not found: ${dir}`);
641
+ }
642
+ for (const name of names.sort()) {
643
+ if (!R.ipofIsInclude(name)) continue;
644
+ const p = join4(dir, name);
645
+ if (resolve(p) === own) continue;
646
+ try {
647
+ if (!statSync2(p).isFile()) continue;
648
+ } catch {
649
+ continue;
650
+ }
651
+ const key = name.toLowerCase();
652
+ if (!(key in files)) files[key] = readFileSync3(p, "latin1");
653
+ }
654
+ }
655
+ return files;
656
+ }
657
+ function readScript(file, includeDirs) {
658
+ const R = loadRuntime();
659
+ const name = basename(file);
660
+ const stem = R.ipofStem(name);
661
+ let bytes;
662
+ try {
663
+ bytes = readFileSync3(file);
664
+ } catch {
665
+ throw new CliError(`cannot read ${file}`);
666
+ }
667
+ if (R.ipofIsCompiled(name)) {
668
+ let exec;
669
+ try {
670
+ exec = R.ipofDecodeExec(new Uint8Array(bytes), stem);
671
+ } catch (e) {
672
+ throw new CliError(String(e.message || e));
673
+ }
674
+ return {
675
+ exec,
676
+ source: "compiled .IPO",
677
+ stem,
678
+ includes: Array.from(exec.includes || [])
679
+ };
680
+ }
681
+ if (!R.ipofIsSource(name))
682
+ throw new CliError(`${name}: not a script (.IPO) or a source (.IPS, .SRC)`);
683
+ const dirs = [dirname2(resolve(file)), ...includeDirs];
684
+ const files = gatherIncludes(dirs, file);
685
+ const r = R.ipofCompileSource(bytes.toString("latin1"), {
686
+ name: stem,
687
+ files
688
+ });
689
+ if (!r.ok || !r.exec) {
690
+ if (r.missing.length) {
691
+ throw new CliError(
692
+ `${name}: missing include${r.missing.length === 1 ? "" : "s"} ${r.missing.join(", ")} (searched ${dirs.join(", ")}; pass -I <dir> with the INPA headers)`
693
+ );
694
+ }
695
+ const first = r.errors[0];
696
+ const where = first && first.line ? `line ${first.line}: ` : "";
697
+ throw new CliError(
698
+ `${name}: ${where}${first ? first.text || first.message : "compile failed"}`
699
+ );
700
+ }
701
+ return {
702
+ exec: r.exec,
703
+ source: "compiled from source",
704
+ stem,
705
+ includes: Array.from(r.includes)
706
+ };
707
+ }
708
+ function ipoInfo(file, includeDirs, json) {
709
+ const R = loadRuntime();
710
+ const s = readScript(file, includeDirs);
711
+ const info = describeScript(s.exec, s.source, R, s.includes);
712
+ if (json) return [JSON.stringify(info, null, 2)];
713
+ const kv = [
714
+ ["Script", `${info.name} (${info.source})`],
715
+ ["Form", info.form],
716
+ [
717
+ "Entry",
718
+ info.entry ? `${info.entry}${info.rootMenu || info.rootScreen ? ` -> ${[info.rootMenu, info.rootScreen].filter(Boolean).join(" / ")}` : ""}` : "none (INPA starts at inpainit or SgbdInpaCheck; this script has neither)"
719
+ ],
720
+ ["Includes", info.includes.length ? info.includes.join(", ") : "none"],
721
+ [
722
+ "Procedures",
723
+ `${info.procedures} (${info.menus.length} menus, ${info.screens} screens, ${info.functions} functions, ${info.machines} state machines)`
724
+ ],
725
+ ["DLL imports", info.imports.length ? info.imports.join(", ") : "none"]
726
+ ];
727
+ if (info.bytes != null) {
728
+ kv.push([
729
+ "Bytecode",
730
+ `${formatCount(info.bytes)} bytes` + (info.unknown ? `, ${formatCount(info.unknown)} undecoded` : "")
731
+ ]);
732
+ }
733
+ const out = formatTable(kv);
734
+ for (const m of info.menus) {
735
+ out.push("");
736
+ out.push(...menuLines(m));
737
+ }
738
+ return out;
739
+ }
740
+ function menuLines(m) {
741
+ const head = [
742
+ m.name,
743
+ m.title ? `"${m.title}"` : "",
744
+ m.screen ? `(screen ${m.screen})` : ""
745
+ ].filter(Boolean).join(" ");
746
+ const rows = m.keys.map((k) => [
747
+ k.key,
748
+ keyLabel(k),
749
+ keyTarget(k),
750
+ keyJobsText(k)
751
+ ]);
752
+ return [head, ...formatTable(rows, void 0, " ")];
753
+ }
754
+ function keyLabel(k) {
755
+ return k.hidden ? "(no caption)" : k.label;
756
+ }
757
+ function keyTarget(k) {
758
+ const parts = [];
759
+ if (k.screen) parts.push(`${k.screen}${k.frequent ? " (cyclic)" : ""}`);
760
+ if (k.submenu) parts.push(`menu ${k.submenu}`);
761
+ if (k.scriptchange) parts.push(`script ${k.scriptchange}`);
762
+ return parts.join(", ");
763
+ }
764
+ function keyJobsText(k) {
765
+ const jobs = k.jobs.map((j) => k.writes.includes(j) ? `${j} [WRITE]` : j);
766
+ const acts = k.actions.filter((a) => a !== "messagebox");
767
+ return [...jobs, ...acts.map((a) => `(${a})`)].join(", ");
768
+ }
769
+ function ipoKeys(file, includeDirs, menu, json) {
770
+ const R = loadRuntime();
771
+ const s = readScript(file, includeDirs);
772
+ const names = menu ? [menu] : menuNames(s.exec);
773
+ if (menu && !(menu in s.exec.procs))
774
+ throw new CliError(
775
+ `${s.stem}: no menu ${menu} (menus: ${menuNames(s.exec).join(", ") || "none"})`
776
+ );
777
+ const menus = names.map((m) => describeMenu(s.exec, m, R));
778
+ if (json) return [JSON.stringify(menus, null, 2)];
779
+ const rows = [];
780
+ for (const m of menus)
781
+ for (const k of m.keys)
782
+ rows.push([
783
+ m.name,
784
+ k.key,
785
+ keyLabel(k),
786
+ keyTarget(k),
787
+ keyJobsText(k),
788
+ k.writes.length ? "yes" : ""
789
+ ]);
790
+ if (!rows.length) return [`${s.stem}: no menu keys`];
791
+ return formatTable(rows, ["MENU", "KEY", "LABEL", "OPENS", "JOBS", "WRITES"]);
792
+ }
793
+ function ipoCompile(file, includeDirs, out) {
794
+ const R = loadRuntime();
795
+ const name = basename(file);
796
+ if (!R.ipofIsSource(name))
797
+ throw new CliError(`${name}: compile takes a .IPS or .SRC source`);
798
+ const s = readScript(file, includeDirs);
799
+ const target = out || join4(dirname2(resolve(file)), `${s.stem}.ipoexec.json`);
800
+ try {
801
+ writeFileSync2(target, JSON.stringify(s.exec));
802
+ } catch {
803
+ throw new CliError(`cannot write ${target}`);
804
+ }
805
+ const inv = R.ipofInventory(s.exec);
806
+ return [
807
+ `${name}: compiled ${Object.keys(s.exec.procs).length} procedures (${inv.menus.length} menus, ${inv.screens.length} screens, ${inv.funcs.length} functions, ${inv.machines.length} state machines)` + (s.includes.length ? `, includes ${s.includes.join(", ")}` : ""),
808
+ `wrote ${target} (the app's exec form; not INPA's binary .IPO)`
809
+ ];
810
+ }
811
+
812
+ // src/live.ts
813
+ import { createInterface } from "node:readline";
814
+
815
+ // src/serial.ts
816
+ import { readdirSync as readdirSync2 } from "node:fs";
817
+ var PORT_PATTERNS = [
818
+ /^cu\.usbserial/i,
819
+ /^cu\.SLAB/i,
820
+ /^cu\.wchusbserial/i,
821
+ /^ttyUSB/i,
822
+ /^ttyACM/i
823
+ ];
824
+ var NodeSerialPort = class {
825
+ path;
826
+ opener;
827
+ binding = null;
828
+ /** chunks heard and not yet read */
829
+ chunks = [];
830
+ /** the one read waiting for bytes, when the queue is empty */
831
+ waiter = null;
832
+ /** the lines as last set, so a partial setSignals keeps the others */
833
+ lines = { dtr: false, rts: false, brk: false };
834
+ /** the wire trace sink, when the CLI wants one */
835
+ info;
836
+ /**
837
+ * @param path - the device path
838
+ * @param opener - how a binding is opened
839
+ * @param info - vendor and product ids, when known (portLabel shows them)
840
+ */
841
+ constructor(path, opener, info = {}) {
842
+ this.path = path;
843
+ this.opener = opener;
844
+ this.info = info;
845
+ }
846
+ /** Is a binding open (Web Serial's SerialPort.connected). */
847
+ get connected() {
848
+ return !!this.binding;
849
+ }
850
+ /**
851
+ * Open the device with the given settings.
852
+ * @param cfg - baud, bits, parity
853
+ */
854
+ async open(cfg) {
855
+ if (this.binding) throw new Error(`${this.path} is already open`);
856
+ const b = await this.opener(this.path, cfg);
857
+ this.binding = b;
858
+ this.chunks = [];
859
+ b.onData((chunk) => this.push(chunk));
860
+ }
861
+ /**
862
+ * Close the device: the binding goes, a waiting read is told `done`.
863
+ */
864
+ async close() {
865
+ const b = this.binding;
866
+ this.binding = null;
867
+ if (this.waiter) {
868
+ const w = this.waiter;
869
+ this.waiter = null;
870
+ w({ value: void 0, done: true });
871
+ }
872
+ this.chunks = [];
873
+ if (b) await b.close();
874
+ }
875
+ /**
876
+ * Bytes the binding heard: to the waiting read, else queued.
877
+ * @param chunk - the bytes
878
+ */
879
+ push(chunk) {
880
+ if (!chunk.length) return;
881
+ if (this.waiter) {
882
+ const w = this.waiter;
883
+ this.waiter = null;
884
+ w({ value: chunk, done: false });
885
+ return;
886
+ }
887
+ this.chunks.push(chunk);
888
+ }
889
+ /** The readable side: getReader() hands back the one reader. */
890
+ get readable() {
891
+ return {
892
+ getReader: () => ({
893
+ read: () => this.read(),
894
+ cancel: async () => this.cancel(),
895
+ releaseLock: () => {
896
+ }
897
+ })
898
+ };
899
+ }
900
+ /** The writable side: getWriter() hands back the one writer. */
901
+ get writable() {
902
+ return {
903
+ getWriter: () => ({
904
+ write: (bytes) => this.write(bytes),
905
+ releaseLock: () => {
906
+ }
907
+ })
908
+ };
909
+ }
910
+ /**
911
+ * One read: the next chunk, or `done` when the port is closed.
912
+ * @returns the read result
913
+ */
914
+ read() {
915
+ const next = this.chunks.shift();
916
+ if (next) return Promise.resolve({ value: next, done: false });
917
+ if (!this.binding) return Promise.resolve({ value: void 0, done: true });
918
+ return new Promise((resolve2) => {
919
+ this.waiter = resolve2;
920
+ });
921
+ }
922
+ /**
923
+ * Cancel the reader: a waiting read is told `done`, buffered bytes go.
924
+ */
925
+ cancel() {
926
+ this.chunks = [];
927
+ if (this.waiter) {
928
+ const w = this.waiter;
929
+ this.waiter = null;
930
+ w({ value: void 0, done: true });
931
+ }
932
+ }
933
+ /**
934
+ * Write bytes. Resolves when the OS has them, not when they have left the
935
+ * wire: the bus then holds DTR for the telegram's own byte time, and
936
+ * waiting for transmission here would double that hold and lose the
937
+ * ECU's answer (the reference interface's DtrTimeCorrCom is 0.3 ms).
938
+ * @param bytes - the framed request
939
+ */
940
+ async write(bytes) {
941
+ if (!this.binding) throw new Error(`${this.path} is not open`);
942
+ await this.binding.write(bytes);
943
+ }
944
+ /**
945
+ * Drive the modem lines. A partial call keeps the lines it does not name,
946
+ * as Web Serial does; the binding always gets all three, because
947
+ * `serialport` would otherwise assert the ones left unsaid.
948
+ * @param s - the lines to set
949
+ */
950
+ async setSignals(s) {
951
+ if (!this.binding) throw new Error(`${this.path} is not open`);
952
+ if (s.dataTerminalReady !== void 0)
953
+ this.lines.dtr = !!s.dataTerminalReady;
954
+ if (s.requestToSend !== void 0) this.lines.rts = !!s.requestToSend;
955
+ if (s.break !== void 0) this.lines.brk = !!s.break;
956
+ await this.binding.set({ ...this.lines });
957
+ }
958
+ /**
959
+ * Read the modem lines (KL15 arrives on DSR or DCD of a K+DCAN cable).
960
+ * @returns the lines, or null when the binding cannot say
961
+ */
962
+ async getSignals() {
963
+ if (!this.binding) return null;
964
+ const st = await this.binding.get();
965
+ if (!st) return null;
966
+ return {
967
+ dataSetReady: !!st.dsr,
968
+ dataCarrierDetect: !!st.dcd,
969
+ clearToSend: !!st.cts
970
+ };
971
+ }
972
+ /**
973
+ * The USB ids, for the bus's port label.
974
+ * @returns what is known
975
+ */
976
+ getInfo() {
977
+ return this.info;
978
+ }
979
+ /** Web Serial's disconnect event; a pulled USB cable is not watched here. */
980
+ addEventListener() {
981
+ }
982
+ };
983
+ var serialportModule = null;
984
+ async function loadSerialport() {
985
+ if (serialportModule) return serialportModule;
986
+ try {
987
+ serialportModule = await import("serialport");
988
+ } catch {
989
+ throw new CliError(
990
+ "the serialport package is not installed; run: npm i -g serialport (or reinstall bmweb-cli with its optional dependencies)"
991
+ );
992
+ }
993
+ return serialportModule;
994
+ }
995
+ async function openSerialportBinding(path, cfg) {
996
+ const mod = await loadSerialport();
997
+ const port = new mod.SerialPort({
998
+ path,
999
+ baudRate: cfg.baudRate,
1000
+ dataBits: cfg.dataBits,
1001
+ stopBits: cfg.stopBits,
1002
+ parity: cfg.parity,
1003
+ autoOpen: false,
1004
+ // keep the lines where we leave them across close/open
1005
+ hupcl: false
1006
+ });
1007
+ await new Promise(
1008
+ (res, rej) => port.open(
1009
+ (e) => e ? rej(new CliError(`cannot open ${path}: ${e.message}`)) : res()
1010
+ )
1011
+ );
1012
+ const call = (fn) => new Promise((res, rej) => fn((e) => e ? rej(e) : res()));
1013
+ const binding = {
1014
+ write: (bytes) => call((cb) => port.write(Buffer.from(bytes), cb)),
1015
+ onData: (fn) => port.on("data", (b) => fn(new Uint8Array(b))),
1016
+ set: (s) => call(
1017
+ (cb) => port.set(
1018
+ {
1019
+ dtr: s.dtr,
1020
+ rts: s.rts,
1021
+ brk: s.brk,
1022
+ cts: false,
1023
+ dsr: false,
1024
+ ...process.platform === "linux" ? { lowLatency: true } : {}
1025
+ },
1026
+ cb
1027
+ )
1028
+ ),
1029
+ get: () => new Promise((res) => port.get((e, st) => res(e || !st ? null : st))),
1030
+ close: () => new Promise((res) => port.close(() => res()))
1031
+ };
1032
+ await binding.set({ dtr: false, rts: false, brk: false });
1033
+ return binding;
1034
+ }
1035
+ async function listPorts(devDir = "/dev") {
1036
+ const found = /* @__PURE__ */ new Map();
1037
+ try {
1038
+ for (const name of readdirSync2(devDir))
1039
+ if (PORT_PATTERNS.some((re) => re.test(name)))
1040
+ found.set(`${devDir}/${name}`, "");
1041
+ } catch {
1042
+ }
1043
+ try {
1044
+ const mod = await loadSerialport();
1045
+ for (const p of await mod.SerialPort.list()) {
1046
+ const base = p.path.split("/").pop() || p.path;
1047
+ if (!PORT_PATTERNS.some((re) => re.test(base))) continue;
1048
+ const detail = [
1049
+ p.manufacturer,
1050
+ p.vendorId && p.productId ? `${p.vendorId}:${p.productId}` : "",
1051
+ p.serialNumber ? `sn ${p.serialNumber}` : ""
1052
+ ].filter(Boolean).join(" ");
1053
+ found.set(p.path, detail);
1054
+ }
1055
+ } catch {
1056
+ }
1057
+ return [...found.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([path, detail]) => ({ path, detail }));
1058
+ }
1059
+ function choosePort(wanted, candidates) {
1060
+ if (wanted) return wanted;
1061
+ if (candidates.length === 1) return candidates[0].path;
1062
+ if (!candidates.length)
1063
+ throw new CliError(
1064
+ "no K+DCAN cable found (looked for cu.usbserial*, cu.SLAB*, cu.wchusbserial*, ttyUSB*, ttyACM*); pass --port <device>"
1065
+ );
1066
+ throw new CliError(
1067
+ `several ports found; pass --port: ${candidates.map((c) => c.path).join(", ")}`
1068
+ );
1069
+ }
1070
+
1071
+ // src/live.ts
1072
+ var FTDI_HINT = "If this is an FTDI cable, set its latency timer to 1 ms (Linux: done for you; macOS: FTDI D2XX/driver setting; Windows: Device Manager, Port Settings, Advanced).";
1073
+ async function connectBus(opts = {}) {
1074
+ configureSite({
1075
+ ...opts.api ? { base: opts.api } : {},
1076
+ refresh: !!opts.refresh
1077
+ });
1078
+ const R = loadRuntime();
1079
+ const g = runtimeGlobals();
1080
+ const candidates = opts.ports || await listPorts();
1081
+ const path = choosePort(opts.port, candidates);
1082
+ const opener = opts.opener || openSerialportBinding;
1083
+ const port = new NodeSerialPort(path, opener);
1084
+ g.navigator.serial = {
1085
+ requestPort: async () => port,
1086
+ getPorts: async () => []
1087
+ };
1088
+ let label;
1089
+ try {
1090
+ label = await R.webBus.connect();
1091
+ } catch (e) {
1092
+ throw new CliError(`cannot open ${path}: ${e.message}`);
1093
+ }
1094
+ return { R, label, path };
1095
+ }
1096
+ async function disconnectBus(R) {
1097
+ try {
1098
+ await R.withBusLock(async () => {
1099
+ });
1100
+ } catch {
1101
+ }
1102
+ if (R.webBus.connected) await R.webBus.disconnect();
1103
+ }
1104
+ async function portsCommand(json, ports) {
1105
+ const list = ports || await listPorts();
1106
+ if (json) return [JSON.stringify(list, null, 2)];
1107
+ if (!list.length)
1108
+ return [
1109
+ "no candidate ports (cu.usbserial*, cu.SLAB*, cu.wchusbserial*, ttyUSB*, ttyACM*)"
1110
+ ];
1111
+ return formatTable(list.map((p) => [p.path, p.detail]));
1112
+ }
1113
+ async function askYesNo(question, io = {}) {
1114
+ const input = io.input || process.stdin;
1115
+ const output = io.output || process.stderr;
1116
+ if (!io.input && !process.stdin.isTTY) return false;
1117
+ const rl = createInterface({ input, output });
1118
+ const answer = await new Promise(
1119
+ (res) => rl.question(`${question} [y/N] `, res)
1120
+ );
1121
+ rl.close();
1122
+ return /^y(es)?$/i.test(answer.trim());
1123
+ }
1124
+ async function jobCommand(sgbd, job, opts = {}) {
1125
+ const R = loadRuntime();
1126
+ const name = job.toUpperCase();
1127
+ const target = sgbd.toLowerCase();
1128
+ if (R.isWriteJob(name) && !opts.yes) {
1129
+ const ask = opts.confirm || askYesNo;
1130
+ const ok = await ask(
1131
+ `${name} on ${target} is a write (it changes the module or drives something). Send it?`
1132
+ );
1133
+ if (!ok)
1134
+ return [
1135
+ `${name} on ${target}: not sent (a write needs --yes or a y answer)`
1136
+ ];
1137
+ }
1138
+ const api = opts.apiFn || runtimeGlobals().api;
1139
+ const q = opts.arg != null && opts.arg !== "" ? `?arg=${encodeURIComponent(opts.arg)}` : "";
1140
+ let d;
1141
+ try {
1142
+ d = await api(`/api/ecu/${target}/run/${encodeURIComponent(name)}${q}`, {
1143
+ method: "POST"
1144
+ });
1145
+ } catch (e) {
1146
+ const m = String(e.message || e);
1147
+ throw new CliError(
1148
+ `${name} on ${target}: ${m}${/IFH-0003/.test(m) ? `
1149
+ ${FTDI_HINT}` : ""}`
1150
+ );
1151
+ }
1152
+ if (opts.json) return [JSON.stringify(d, null, 2)];
1153
+ return formatAnswer(d);
1154
+ }
1155
+ function formatAnswer(d) {
1156
+ const sys = d.system || {};
1157
+ const out = [
1158
+ `${String(sys.OBJECT || "")} ${String(sys.VARIANTE || "")} ${String(sys.JOBNAME || d.job || "")}: ${(d.sets || []).length} set${(d.sets || []).length === 1 ? "" : "s"}`
1159
+ ];
1160
+ (d.sets || []).forEach((set, i) => {
1161
+ out.push("", `set ${i + 1}`);
1162
+ const rows = Object.entries(set).filter(([k]) => !k.startsWith("_")).map(([k, v]) => [k, valueText(v)]);
1163
+ out.push(...formatTable(rows, void 0, " "));
1164
+ });
1165
+ return out;
1166
+ }
1167
+ function valueText(v) {
1168
+ if (v == null) return "";
1169
+ if (Array.isArray(v))
1170
+ return v.map(
1171
+ (b) => (Number(b) & 255).toString(16).toUpperCase().padStart(2, "0")
1172
+ ).join("-");
1173
+ if (typeof v === "object") return JSON.stringify(v);
1174
+ return String(v);
1175
+ }
1176
+ async function openModule(chassis, sgbd, api = runtimeGlobals().api) {
1177
+ const cid = chassis.toUpperCase();
1178
+ const want = sgbd.toLowerCase();
1179
+ let ecu;
1180
+ if (want === cid.toLowerCase()) {
1181
+ ecu = {
1182
+ code: cid,
1183
+ sgbd: want,
1184
+ label: `INPA ${cid} script`,
1185
+ group: null,
1186
+ kind: "vehicle",
1187
+ chassis: cid
1188
+ };
1189
+ } else {
1190
+ let config2;
1191
+ try {
1192
+ config2 = await api(`/api/chassis/${cid}`);
1193
+ } catch (e) {
1194
+ throw new CliError(
1195
+ `no chassis ${cid} on the site (${e.message})`
1196
+ );
1197
+ }
1198
+ const rows = [];
1199
+ for (const s of config2.sections || []) rows.push(...s.ecus || []);
1200
+ const row = rows.find((r) => String(r.sgbd).toLowerCase() === want) || rows.find((r) => String(r.code || "").toLowerCase() === want) || rows.find(
1201
+ (r) => (r.variants || []).some((v) => String(v).toLowerCase() === want)
1202
+ );
1203
+ if (!row)
1204
+ throw new CliError(
1205
+ `${cid} carries no module ${sgbd} (try: ${rows.slice(0, 12).map((r) => r.sgbd).join(", ")}${rows.length > 12 ? ", ..." : ""})`
1206
+ );
1207
+ ecu = {
1208
+ code: row.code || "",
1209
+ label: row.label || row.sgbd,
1210
+ sgbd: String(row.sgbd).toLowerCase(),
1211
+ group: row.group || null,
1212
+ chassis: cid
1213
+ };
1214
+ }
1215
+ return prepareModule(ecu, api);
1216
+ }
1217
+ async function prepareModule(ecu, api = runtimeGlobals().api) {
1218
+ const R = loadRuntime();
1219
+ try {
1220
+ const idx = await api("/api/ecu-index.json");
1221
+ ecu._ipoKnownSgbds = new Set(
1222
+ Object.keys(idx || {}).map((k) => k.toLowerCase())
1223
+ );
1224
+ } catch {
1225
+ ecu._ipoKnownSgbds = /* @__PURE__ */ new Set();
1226
+ }
1227
+ await resolveGroupVariant(ecu, api, R);
1228
+ const codeHint = ecu.code ? `?code=${encodeURIComponent(ecu.code)}` : "";
1229
+ ecu._ir = await api(`/api/ecu/${ecu.sgbd}/ir${codeHint}`).catch(() => null);
1230
+ const menusOf = (ir) => Object.keys((ir || {}).menus || {}).length;
1231
+ if (!menusOf(ecu._ir) && ecu._sgbdBase && ecu._sgbdBase !== ecu.sgbd) {
1232
+ const base = await api(`/api/ecu/${ecu._sgbdBase}/ir${codeHint}`).catch(
1233
+ () => null
1234
+ );
1235
+ if (menusOf(base)) {
1236
+ ecu._ir = base;
1237
+ ecu._irFrom = ecu._sgbdBase;
1238
+ }
1239
+ }
1240
+ const script = String(ecu._irFrom || ecu.sgbd).toLowerCase();
1241
+ const exec = await R.irLiveExec(script);
1242
+ if (!exec || !exec.procs || !Object.keys(exec.procs).length)
1243
+ throw new CliError(
1244
+ `${script}: the site ships no runnable script for this module`
1245
+ );
1246
+ if (!(exec.procs.inpainit || exec.procs.SgbdInpaCheck))
1247
+ throw new CliError(
1248
+ `${script}: the script has no INPA entry point (inpainit)`
1249
+ );
1250
+ return { ecu, exec };
1251
+ }
1252
+ async function resolveGroupVariant(ecu, api, R) {
1253
+ const g = String(ecu.group || "").toLowerCase();
1254
+ ecu._variantSource = null;
1255
+ if (!g) {
1256
+ ecu._variantSource = "ungrouped";
1257
+ return;
1258
+ }
1259
+ let idx = null;
1260
+ try {
1261
+ idx = await api("/data/groups/index.json");
1262
+ } catch {
1263
+ idx = null;
1264
+ }
1265
+ if (!idx || !(idx.groups || []).includes(g)) {
1266
+ ecu._variantSource = "nogroup";
1267
+ return;
1268
+ }
1269
+ let v = null;
1270
+ try {
1271
+ v = await R.webResolveVariant(g);
1272
+ } catch {
1273
+ v = null;
1274
+ }
1275
+ if (!v) {
1276
+ ecu._variantSource = "unverified";
1277
+ return;
1278
+ }
1279
+ ecu._variant = v.toUpperCase();
1280
+ ecu._variantSource = "confirmed";
1281
+ if (v === String(ecu.sgbd).toLowerCase()) return;
1282
+ try {
1283
+ const jobs = await api(`/api/ecu/${v}/jobs`);
1284
+ if (!Array.isArray(jobs) || !jobs.length) return;
1285
+ } catch {
1286
+ return;
1287
+ }
1288
+ ecu._sgbdBase = ecu._sgbdBase || ecu.sgbd;
1289
+ ecu.sgbd = v;
1290
+ ecu._variantSource = "identified";
1291
+ }
1292
+ var SCAN_CHASSIS = [
1293
+ "E46",
1294
+ "E53",
1295
+ "E65",
1296
+ "E83",
1297
+ "E85",
1298
+ "E87",
1299
+ "E89",
1300
+ "E90",
1301
+ "R50",
1302
+ "R56"
1303
+ ];
1304
+
1305
+ // src/faults.ts
1306
+ function hexDigits(v) {
1307
+ if (v == null || v === "") return "";
1308
+ const text = Array.isArray(v) ? v.map((b) => (Number(b) & 255).toString(16).padStart(2, "0")).join("") : String(v);
1309
+ return text.trim().toUpperCase().replace(/^0X/, "").replace(/[^0-9A-F]/g, "").slice(0, 4);
1310
+ }
1311
+ function leadingCode(text) {
1312
+ const m = /^([0-9A-F]{3,5})\b/i.exec(String(text || "").trim());
1313
+ return m ? m[1].toUpperCase() : "";
1314
+ }
1315
+ function faultCode(c) {
1316
+ const hex = hexDigits(c.F_HEX_CODE);
1317
+ if (hex) return hex;
1318
+ const lead = leadingCode(c.F_ORT_TEXT);
1319
+ if (lead) return lead;
1320
+ const nr = c.F_ORT_NR;
1321
+ if (nr != null && String(nr).trim() !== "") return String(nr).trim();
1322
+ return "";
1323
+ }
1324
+ function faultText(c, code) {
1325
+ const text = String(c.F_ORT_TEXT || "").trim();
1326
+ const lead = leadingCode(text);
1327
+ if (lead && lead === code) return text.slice(lead.length).trim();
1328
+ return text;
1329
+ }
1330
+ function faultCount(c) {
1331
+ const n = c.F_HFK ?? c.F_LZ;
1332
+ return n == null || n === "" ? "" : String(n);
1333
+ }
1334
+ function faultState(c) {
1335
+ const vt = String(c.F_VORHANDEN_TEXT || "").toLowerCase();
1336
+ if (!vt) return "";
1337
+ const says = /momentan vorhanden|currently present/.test(vt);
1338
+ const denies = /nicht vorhanden|not (currently )?present/.test(vt);
1339
+ return says && !denies ? "present" : "stored";
1340
+ }
1341
+ function faultRow(c) {
1342
+ const code = faultCode(c);
1343
+ return {
1344
+ code,
1345
+ text: faultText(c, code),
1346
+ count: faultCount(c),
1347
+ state: faultState(c)
1348
+ };
1349
+ }
1350
+
1351
+ // src/report.ts
1352
+ function payloadOf(arg) {
1353
+ const s = String(arg || "").trim();
1354
+ const at = s.indexOf("#report/");
1355
+ if (at >= 0) return s.slice(at + "#report/".length);
1356
+ if (s.startsWith("report/")) return s.slice("report/".length);
1357
+ if (/^https?:\/\//i.test(s))
1358
+ throw new CliError("that link carries no #report/ fragment");
1359
+ return s;
1360
+ }
1361
+ async function decodeReport(arg) {
1362
+ const R = loadRuntime();
1363
+ const payload = payloadOf(arg);
1364
+ if (!payload) throw new CliError("no report payload given");
1365
+ const p = await R.garageShareDecode(payload);
1366
+ if (!p)
1367
+ throw new CliError(
1368
+ "this link does not carry a readable report (it may have been cut short when pasted)"
1369
+ );
1370
+ return p;
1371
+ }
1372
+ function scanOf(p) {
1373
+ const R = loadRuntime();
1374
+ return {
1375
+ kind: p.kind,
1376
+ at: p.at,
1377
+ chassis: p.chassis,
1378
+ report: p.report,
1379
+ summary: p.summary || R.garageScanSummary(p.report)
1380
+ };
1381
+ }
1382
+ function moduleName(m) {
1383
+ return m.label || String(m.sgbd || "").toUpperCase();
1384
+ }
1385
+ function identRows(ident) {
1386
+ const R = loadRuntime();
1387
+ const rows = [];
1388
+ const seen = /* @__PURE__ */ new Set();
1389
+ const val = (k) => ident[k] == null ? "" : String(ident[k]).trim();
1390
+ for (const [keys, cap] of R.IPO_IDENT_ROWS || []) {
1391
+ for (const k of keys) {
1392
+ seen.add(k);
1393
+ const v = val(k);
1394
+ if (v) rows.push([keys.length > 1 ? `${cap} (${k})` : cap, k, v]);
1395
+ }
1396
+ }
1397
+ for (const k of Object.keys(ident)) {
1398
+ if (seen.has(k) || k.startsWith("_")) continue;
1399
+ const v = val(k);
1400
+ if (v) rows.push([k, k, v]);
1401
+ }
1402
+ return rows;
1403
+ }
1404
+ function headerLines(p) {
1405
+ const R = loadRuntime();
1406
+ const sum = p.summary || R.garageScanSummary(p.report);
1407
+ const who = [p.label, p.chassis].filter(Boolean).join(" / ");
1408
+ const kind = p.kind === "ident" ? "identification" : "fault memories";
1409
+ return formatTable([
1410
+ ["Report", `${kind}${who ? ` of ${who}` : ""}`],
1411
+ ["Read", p.at || ""],
1412
+ [
1413
+ "Modules",
1414
+ `${sum.modules} read, ${sum.withFaults} with faults, ${sum.faults} fault${sum.faults === 1 ? "" : "s"}, ${sum.silent} silent`
1415
+ ]
1416
+ ]);
1417
+ }
1418
+ function faultLines(report) {
1419
+ const rows = [];
1420
+ for (const m of report.modules || []) {
1421
+ const codes = m.codes || [];
1422
+ if (!codes.length) {
1423
+ rows.push([moduleName(m), "", "no faults stored"]);
1424
+ continue;
1425
+ }
1426
+ for (const c of codes) {
1427
+ const r = faultRow(c);
1428
+ rows.push([moduleName(m), r.code, r.text, r.count, r.state]);
1429
+ }
1430
+ }
1431
+ if (!rows.length) return ["no modules in this report"];
1432
+ return formatTable(rows, ["MODULE", "CODE", "TEXT", "COUNT", "STATE"]);
1433
+ }
1434
+ function identLines(report) {
1435
+ const rows = [];
1436
+ for (const m of report.modules || []) {
1437
+ const ident = m.ident;
1438
+ if (!ident || !Object.keys(ident).length) {
1439
+ rows.push([moduleName(m), "", "(no identification)"]);
1440
+ continue;
1441
+ }
1442
+ for (const [cap, , v] of identRows(ident))
1443
+ rows.push([moduleName(m), cap, v]);
1444
+ }
1445
+ if (!rows.length) return ["no modules in this report"];
1446
+ return formatTable(rows, ["MODULE", "FIELD", "VALUE"]);
1447
+ }
1448
+ function silentLines(report) {
1449
+ const silent = report.silent || [];
1450
+ if (!silent.length) return [];
1451
+ return [
1452
+ "",
1453
+ `Silent (${silent.length}):`,
1454
+ ...formatTable(
1455
+ silent.map((s) => [
1456
+ String(s.target || "").toUpperCase(),
1457
+ s.label || "",
1458
+ s.error || ""
1459
+ ]),
1460
+ void 0,
1461
+ " "
1462
+ )
1463
+ ];
1464
+ }
1465
+ async function reportShow(arg, json) {
1466
+ const p = await decodeReport(arg);
1467
+ if (json) return [JSON.stringify(p, null, 2)];
1468
+ return [...headerLines(p), "", ...reportBodyLines(p.report, p.kind)];
1469
+ }
1470
+ function reportBodyLines(report, kind) {
1471
+ const body = kind === "ident" ? identLines(report) : faultLines(report);
1472
+ return [...body, ...silentLines(report)];
1473
+ }
1474
+ function diffFaultRow(sign, c) {
1475
+ const r = faultRow(c);
1476
+ return [sign, r.code, r.text, r.count];
1477
+ }
1478
+ function diffJson(a, b, diff) {
1479
+ const R = loadRuntime();
1480
+ const strip = (p) => ({
1481
+ kind: p.kind,
1482
+ at: p.at,
1483
+ chassis: p.chassis,
1484
+ label: p.label
1485
+ });
1486
+ return {
1487
+ from: strip(a),
1488
+ to: strip(b),
1489
+ kind: diff.kind,
1490
+ counts: R.garageDiffCounts(diff),
1491
+ modules: diff.modules,
1492
+ silence: diff.silence
1493
+ };
1494
+ }
1495
+ async function reportDiff(argA, argB, json) {
1496
+ const R = loadRuntime();
1497
+ const a = await decodeReport(argA);
1498
+ const b = await decodeReport(argB);
1499
+ const diff = R.garageDiffScans(scanOf(a), scanOf(b));
1500
+ if (json) return [JSON.stringify(diffJson(a, b, diff), null, 2)];
1501
+ const counts = R.garageDiffCounts(diff);
1502
+ const out = formatTable([
1503
+ ["From", `${a.at || ""}${a.label ? ` ${a.label}` : ""}`],
1504
+ ["To", `${b.at || ""}${b.label ? ` ${b.label}` : ""}`],
1505
+ [
1506
+ "Changes",
1507
+ `${counts.added} new, ${counts.cleared} cleared, ${counts.same} still present` + (diff.kind !== "faults" ? `, ${counts.fields} ident fields changed` : "") + `, ${counts.modules} module${counts.modules === 1 ? "" : "s"} changed`
1508
+ ]
1509
+ ]);
1510
+ for (const m of diff.modules) {
1511
+ out.push("");
1512
+ if (m.unread) {
1513
+ out.push(`${m.label} (not read in the newer report)`);
1514
+ continue;
1515
+ }
1516
+ if (!m.changed) {
1517
+ out.push(
1518
+ `${m.label} unchanged` + (m.same.length ? ` (${m.same.length} still present)` : "")
1519
+ );
1520
+ continue;
1521
+ }
1522
+ out.push(m.label);
1523
+ const rows = [];
1524
+ for (const c of m.added) rows.push(diffFaultRow("+", c));
1525
+ for (const c of m.cleared) rows.push(diffFaultRow("-", c));
1526
+ for (const c of m.same) rows.push(diffFaultRow("=", c));
1527
+ for (const f of m.fields)
1528
+ rows.push(["~", f.label, `${f.from || "(none)"} -> ${f.to || "(none)"}`]);
1529
+ out.push(...formatTable(rows, void 0, " "));
1530
+ }
1531
+ if (diff.silence.length) {
1532
+ out.push("", "Answering changed:");
1533
+ out.push(
1534
+ ...formatTable(
1535
+ diff.silence.map((s) => [
1536
+ s.state === "silent" ? "silent now" : "answering now",
1537
+ s.label,
1538
+ s.error || ""
1539
+ ]),
1540
+ void 0,
1541
+ " "
1542
+ )
1543
+ );
1544
+ }
1545
+ out.push("", "+ new - cleared = still present ~ ident field changed");
1546
+ return out;
1547
+ }
1548
+
1549
+ // src/scan.ts
1550
+ var VEHICLE_SCRIPTS = {
1551
+ E46: "e46",
1552
+ E53: "e53",
1553
+ E65: "e65",
1554
+ E83: "e83",
1555
+ E85: "e85",
1556
+ E87: "e87",
1557
+ E89: "e89_d_all",
1558
+ E90: "e90",
1559
+ R50: "r50",
1560
+ R56: "r56_all"
1561
+ };
1562
+ var FAULT_KEY_RE = /^(FS lesen|Fehlerspeicher lesen|Read (fault|error) memory)$/i;
1563
+ var FAULT_MENU = "m_fs";
1564
+ function headlessUi(progress) {
1565
+ let lastBox = "";
1566
+ return {
1567
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
1568
+ loadExec: async (sgbd) => loadRuntime().irLiveExec(sgbd),
1569
+ route: () => {
1570
+ },
1571
+ status: () => {
1572
+ },
1573
+ error: (_p, text) => progress(`error: ${text}`),
1574
+ message: async (title, body) => {
1575
+ progress(`${title}${body ? `: ${body}` : ""}`);
1576
+ },
1577
+ // a prompt cannot be answered headless: the key is abandoned
1578
+ askInput: async () => null,
1579
+ // a scan reads; a key that would write is declined, never sent
1580
+ confirmKey: async () => false,
1581
+ confirmWrite: async () => false,
1582
+ pickComponent: async () => null,
1583
+ pickLines: async () => null,
1584
+ saveFile: async () => null,
1585
+ writeFile: async () => {
1586
+ },
1587
+ printScreen: () => {
1588
+ },
1589
+ resolveScriptEcu: (from, script, exec) => loadRuntime().ipoResolveScriptEcu(from, script, exec),
1590
+ machineTick: async () => "tick",
1591
+ userbox: (_p, box) => {
1592
+ if (!box) return;
1593
+ const lines = box.lines || [];
1594
+ const text = lines.length ? String(lines[lines.length - 1]) : "";
1595
+ if (text && text !== lastBox) {
1596
+ lastBox = text;
1597
+ progress(text.trim());
1598
+ }
1599
+ },
1600
+ renderKeys: () => {
1601
+ },
1602
+ paint: () => {
1603
+ },
1604
+ left: () => {
1605
+ }
1606
+ };
1607
+ }
1608
+ async function scanCommand(chassis, opts = {}) {
1609
+ const R = loadRuntime();
1610
+ const cid = chassis.toUpperCase();
1611
+ if (opts.apiFn) setApiImpl(opts.apiFn);
1612
+ try {
1613
+ return await scanRun(R, cid, opts);
1614
+ } finally {
1615
+ if (opts.apiFn) setApiImpl(null);
1616
+ }
1617
+ }
1618
+ async function scanRun(R, cid, opts) {
1619
+ const progress = opts.progress || ((l) => process.stderr.write(`${l}
1620
+ `));
1621
+ const api = opts.apiFn || runtimeGlobals().api;
1622
+ let ecu;
1623
+ let exec;
1624
+ if (opts.exec) {
1625
+ ecu = opts.ecu || {
1626
+ code: cid,
1627
+ sgbd: cid.toLowerCase(),
1628
+ label: `INPA ${cid} script`,
1629
+ group: null,
1630
+ kind: "vehicle",
1631
+ chassis: cid
1632
+ };
1633
+ exec = opts.exec;
1634
+ } else {
1635
+ const script = VEHICLE_SCRIPTS[cid];
1636
+ if (!script)
1637
+ throw new CliError(
1638
+ `no whole-vehicle script for ${cid} (one ships for ${Object.keys(VEHICLE_SCRIPTS).join(" ")})`
1639
+ );
1640
+ ({ ecu, exec } = await openModule(cid, script, api));
1641
+ ecu.kind = "vehicle";
1642
+ }
1643
+ const ui = headlessUi(progress);
1644
+ const p = new R.IpoProgram(ecu, exec, ui);
1645
+ const r = await p.start();
1646
+ if (p.noCable) throw new CliError("no cable connected");
1647
+ if (!r.ok) {
1648
+ const last = p.messages[p.messages.length - 1];
1649
+ throw new CliError(
1650
+ `${cid}: the script did not start (${r.reason || "stopped"})` + (last ? `: ${last.title}${last.body ? ` ${last.body}` : ""}` : "")
1651
+ );
1652
+ }
1653
+ if (exec.procs[FAULT_MENU] && p.menu !== FAULT_MENU)
1654
+ await p.openMenu(FAULT_MENU);
1655
+ const key = p.items.find(
1656
+ (it) => FAULT_KEY_RE.test(String(it.label || it.legendLabel || "").trim())
1657
+ );
1658
+ if (!key) {
1659
+ const have = p.items.map((it) => `F${it.nr} ${it.label}`).join(", ");
1660
+ p.close();
1661
+ throw new CliError(
1662
+ `${cid}: no fault-memory read key on ${p.menu} (keys: ${have})`
1663
+ );
1664
+ }
1665
+ progress(`${cid}: ${key.label} (F${key.nr})`);
1666
+ await p.press(key.nr);
1667
+ const report = p.view && p.view.report || R.ipoProtocolReport(p.wireReads, p.view && p.view.lines || []);
1668
+ await p.leaveModule();
1669
+ const summary = R.garageScanSummary(report);
1670
+ const at = (/* @__PURE__ */ new Date()).toISOString();
1671
+ let link;
1672
+ if (opts.share) {
1673
+ const payload = await R.garageShareEncode(
1674
+ { kind: report.kind, at, chassis: cid, report, summary },
1675
+ { label: opts.label || "", chassis: cid }
1676
+ );
1677
+ link = `https://bmweb.danner.ink/#report/${payload}`;
1678
+ }
1679
+ if (opts.json) {
1680
+ return {
1681
+ report,
1682
+ link,
1683
+ lines: [
1684
+ JSON.stringify({ chassis: cid, at, summary, report, link }, null, 2)
1685
+ ]
1686
+ };
1687
+ }
1688
+ const lines = formatTable([
1689
+ ["Scan", `${cid} fault memories (${ecu.sgbd}.ipo)`],
1690
+ ["Read", at],
1691
+ [
1692
+ "Modules",
1693
+ `${summary.modules} answered, ${summary.withFaults} with faults, ${summary.faults} fault${summary.faults === 1 ? "" : "s"}, ${summary.silent} silent`
1694
+ ]
1695
+ ]);
1696
+ lines.push("", ...reportBodyLines(report, report.kind));
1697
+ if (link) lines.push("", `Share: ${link}`);
1698
+ return { report, lines, link };
1699
+ }
1700
+
1701
+ // src/search.ts
1702
+ import {
1703
+ existsSync as existsSync2,
1704
+ mkdirSync as mkdirSync2,
1705
+ readFileSync as readFileSync4,
1706
+ statSync as statSync3,
1707
+ writeFileSync as writeFileSync3
1708
+ } from "node:fs";
1709
+ import { gunzipSync } from "node:zlib";
1710
+ var SITE = "https://bmweb.danner.ink/";
1711
+ var INDEX_URL = `${SITE}api/search-index.json.gz`;
1712
+ var INDEX_MAX_AGE_MS = CACHE_MAX_AGE_MS;
1713
+ function parseIndex(gz) {
1714
+ const R = loadRuntime();
1715
+ let doc;
1716
+ try {
1717
+ doc = JSON.parse(gunzipSync(gz).toString("utf8"));
1718
+ } catch {
1719
+ throw new CliError("the search index is not readable (not gzipped JSON)");
1720
+ }
1721
+ if (!doc || doc.v !== R.SEARCH_INDEX_VERSION)
1722
+ throw new CliError(
1723
+ `the search index is format v${doc && doc.v}; this bmweb-cli reads v${R.SEARCH_INDEX_VERSION} (update the package)`
1724
+ );
1725
+ return doc;
1726
+ }
1727
+ async function loadIndex(opts = {}) {
1728
+ const env = opts.env || process.env;
1729
+ const file = indexCachePath(env);
1730
+ const warn = opts.warn || ((line) => console.error(line));
1731
+ const have = existsSync2(file);
1732
+ const fresh = have && Date.now() - statSync3(file).mtimeMs < INDEX_MAX_AGE_MS;
1733
+ if (have && fresh && !opts.refresh) return parseIndex(readFileSync4(file));
1734
+ const doFetch = opts.fetchImpl || fetch;
1735
+ let bytes = null;
1736
+ let why = "";
1737
+ try {
1738
+ const r = await doFetch(INDEX_URL);
1739
+ if (r.ok) bytes = new Uint8Array(await r.arrayBuffer());
1740
+ else why = `HTTP ${r.status}`;
1741
+ } catch (e) {
1742
+ why = String(e.message || e);
1743
+ }
1744
+ if (bytes) {
1745
+ const index = parseIndex(bytes);
1746
+ mkdirSync2(cacheDir(env), { recursive: true });
1747
+ writeFileSync3(file, bytes);
1748
+ return index;
1749
+ }
1750
+ if (have) {
1751
+ warn(
1752
+ `bmweb: could not refresh the search index (${why}); using the cached copy`
1753
+ );
1754
+ return parseIndex(readFileSync4(file));
1755
+ }
1756
+ throw new CliError(
1757
+ `could not fetch the search index from ${INDEX_URL} (${why})`
1758
+ );
1759
+ }
1760
+ function hitLink(hit, chassis) {
1761
+ const R = loadRuntime();
1762
+ const route = R.searchHitRoute(hit, chassis);
1763
+ return route ? `${SITE}#${route}` : null;
1764
+ }
1765
+ function searchRows(result) {
1766
+ const rows = [];
1767
+ for (const g of result.groups)
1768
+ for (const m of g.modules)
1769
+ for (const h of m.hits) {
1770
+ const e = h.entry;
1771
+ rows.push({
1772
+ chassis: g.chassis,
1773
+ sgbd: m.module.sgbd,
1774
+ module: m.module.label,
1775
+ code: m.module.code,
1776
+ kind: e.t === "k" ? "key" : "screen",
1777
+ key: e.t === "k" && e.n != null ? keyText(e.n) : null,
1778
+ menu: e.t === "k" ? e.m || null : null,
1779
+ screen: e.s || null,
1780
+ label: h.label,
1781
+ title: h.sub,
1782
+ jobs: h.jobs,
1783
+ writes: !!e.w,
1784
+ link: hitLink(h, g.chassis),
1785
+ score: h.score
1786
+ });
1787
+ }
1788
+ return rows;
1789
+ }
1790
+ function keyText(n) {
1791
+ return n > 10 ? `Shift+F${n - 10}` : `F${n}`;
1792
+ }
1793
+ function runSearch(query, opts) {
1794
+ const R = loadRuntime();
1795
+ if (!R.searchTerms(query).length)
1796
+ throw new CliError("the query needs at least two characters");
1797
+ const result = R.searchRun(opts.index, query, {
1798
+ chassis: opts.chassis,
1799
+ max: opts.limit
1800
+ });
1801
+ const rows = searchRows(result);
1802
+ if (opts.json)
1803
+ return [
1804
+ JSON.stringify(
1805
+ { query, total: result.total, shown: result.shown, rows },
1806
+ null,
1807
+ 2
1808
+ )
1809
+ ];
1810
+ if (!rows.length) return [`no results for "${query}"`];
1811
+ const out = [];
1812
+ for (const g of result.groups) {
1813
+ out.push(
1814
+ g.chassis || "(modules no car carries; open them by name in the app)"
1815
+ );
1816
+ for (const m of g.modules) {
1817
+ const head = [
1818
+ m.module.sgbd,
1819
+ m.module.label,
1820
+ m.module.code ? `(${m.module.code})` : ""
1821
+ ].filter(Boolean).join(" ");
1822
+ out.push(` ${head}`);
1823
+ const table = m.hits.map((h) => {
1824
+ const e = h.entry;
1825
+ const what = e.t === "k" && e.n != null ? keyText(e.n) : "screen";
1826
+ const label = h.label + (h.sub ? ` (${h.sub})` : "");
1827
+ const jobs = h.jobs.join(", ") + (e.w ? " [WRITE]" : "");
1828
+ const link = hitLink(h, g.chassis) || "(not openable)";
1829
+ return [what, label, jobs, link];
1830
+ });
1831
+ out.push(...formatTable(table, void 0, " "));
1832
+ }
1833
+ out.push("");
1834
+ }
1835
+ out.push(
1836
+ result.shown < result.total ? `${result.shown} of ${result.total} results shown (raise --limit for more)` : `${result.total} result${result.total === 1 ? "" : "s"}`
1837
+ );
1838
+ return out;
1839
+ }
1840
+
1841
+ // src/tui.ts
1842
+ import { writeFileSync as writeFileSync4 } from "node:fs";
1843
+ import { createInterface as createInterface2, emitKeypressEvents } from "node:readline";
1844
+ function terminalHomeHost(api, status) {
1845
+ return {
1846
+ chassis: async () => {
1847
+ const ids = await api("/api/chassis");
1848
+ return (ids || []).map((id) => ({ value: id, label: id }));
1849
+ },
1850
+ modules: async (chassis) => {
1851
+ const ch = await api(`/api/chassis/${encodeURIComponent(chassis)}`);
1852
+ const out = [];
1853
+ for (const sec of ch && ch.sections || [])
1854
+ for (const ecu of sec.ecus || [])
1855
+ if (ecu && ecu.sgbd)
1856
+ out.push({
1857
+ value: String(ecu.sgbd).toLowerCase(),
1858
+ label: ecu.label || ecu.code || ecu.sgbd,
1859
+ meta: `${sec.name || ""} ${ecu.sgbd}`
1860
+ });
1861
+ return out;
1862
+ },
1863
+ vehicle: async (chassis) => VEHICLE_SCRIPTS[String(chassis).toUpperCase()] || "",
1864
+ status
1865
+ };
1866
+ }
1867
+ var SHIFTED_DIGITS = "!@#$%^&*()";
1868
+ var DIGITS = "1234567890";
1869
+ var GAUGE_WIDTH = 12;
1870
+ function keyToPress(k) {
1871
+ if (k.ctrl && k.name === "c") return "quit";
1872
+ if (k.name === "escape") return "back";
1873
+ if (k.ch === "q" || k.ch === "Q") return "quit";
1874
+ const f = /^f(\d{1,2})$/.exec(k.name);
1875
+ if (f) {
1876
+ const n = Number(f[1]);
1877
+ if (n >= 1 && n <= 10) return k.shift ? n + 10 : n;
1878
+ return null;
1879
+ }
1880
+ const d = DIGITS.indexOf(k.ch);
1881
+ if (k.ch && d >= 0) return d + 1;
1882
+ const s = SHIFTED_DIGITS.indexOf(k.ch);
1883
+ if (k.ch && s >= 0) return s + 11;
1884
+ return null;
1885
+ }
1886
+ function nodeTerminal() {
1887
+ const input = process.stdin;
1888
+ const output = process.stdout;
1889
+ emitKeypressEvents(input);
1890
+ const raw = (on) => {
1891
+ if (input.isTTY) input.setRawMode(on);
1892
+ };
1893
+ raw(true);
1894
+ input.resume();
1895
+ const subs = /* @__PURE__ */ new Set();
1896
+ let paused = false;
1897
+ input.on(
1898
+ "keypress",
1899
+ (str, key) => {
1900
+ if (paused) return;
1901
+ const k = {
1902
+ name: key && key.name || "",
1903
+ ch: str || "",
1904
+ shift: !!(key && key.shift),
1905
+ ctrl: !!(key && key.ctrl)
1906
+ };
1907
+ for (const f of subs) f(k);
1908
+ }
1909
+ );
1910
+ return {
1911
+ write: (s) => {
1912
+ output.write(s);
1913
+ },
1914
+ get columns() {
1915
+ return output.columns || 80;
1916
+ },
1917
+ get rows() {
1918
+ return output.rows || 24;
1919
+ },
1920
+ onKey(fn) {
1921
+ subs.add(fn);
1922
+ return () => {
1923
+ subs.delete(fn);
1924
+ };
1925
+ },
1926
+ async readLine(prompt) {
1927
+ paused = true;
1928
+ raw(false);
1929
+ const rl = createInterface2({ input, output, terminal: true });
1930
+ try {
1931
+ return await new Promise((res) => {
1932
+ rl.question(prompt, (a) => res(a));
1933
+ rl.on("close", () => res(null));
1934
+ });
1935
+ } finally {
1936
+ rl.close();
1937
+ raw(true);
1938
+ paused = false;
1939
+ }
1940
+ },
1941
+ close() {
1942
+ raw(false);
1943
+ input.pause();
1944
+ }
1945
+ };
1946
+ }
1947
+ var TuiUi = class {
1948
+ term;
1949
+ sent = [];
1950
+ statusText = "";
1951
+ progressText = "";
1952
+ stopRequested = false;
1953
+ writeKeys = /* @__PURE__ */ new Set();
1954
+ program = null;
1955
+ leftResolve = null;
1956
+ /** resolves once the program reports it left the module */
1957
+ leftPromise;
1958
+ R;
1959
+ /** the home script's host: what its picks list */
1960
+ home;
1961
+ /**
1962
+ * @param term - the terminal
1963
+ * @param R - the runtime (for the write verdict on key captions)
1964
+ * @param home - the home script's host, when the home may run
1965
+ */
1966
+ constructor(term, R, home = null) {
1967
+ this.term = term;
1968
+ this.R = R;
1969
+ this.home = home;
1970
+ this.leftPromise = new Promise((res) => {
1971
+ this.leftResolve = res;
1972
+ });
1973
+ }
1974
+ /**
1975
+ * BMWeb's own picker (the home script's bmweb_pick): the host's list as
1976
+ * numbered rows; a number picks, anything else narrows the list to the
1977
+ * rows containing it, an empty answer cancels. A vehicle pick has one
1978
+ * answer and asks nothing.
1979
+ * @param _p - the program
1980
+ * @param step - the pick suspension
1981
+ * @returns the chosen value, or null for cancel
1982
+ */
1983
+ async pickHome(_p, step) {
1984
+ if (!this.home) return null;
1985
+ if (step.what === "vehicle") return this.home.vehicle(step.arg);
1986
+ let options;
1987
+ try {
1988
+ options = step.what === "module" ? await this.home.modules(step.arg) : await this.home.chassis();
1989
+ } catch (e) {
1990
+ await this.message("No list", String(e.message || e));
1991
+ return null;
1992
+ }
1993
+ const title = step.what === "module" ? `Modules of ${step.arg}` : "Vehicles";
1994
+ let shown = options;
1995
+ for (; ; ) {
1996
+ const rows = shown.map(
1997
+ (o, i) => ` ${String(i + 1).padStart(3)}. ${o.label}${o.meta ? ` (${o.meta})` : ""}`
1998
+ ).join("\n");
1999
+ const a = await this.term.readLine(
2000
+ `
2001
+ ${title}${shown.length !== options.length ? ` (${shown.length} of ${options.length})` : ""}
2002
+ ${rows}
2003
+ Number to open, text to filter, Enter to cancel: `
2004
+ );
2005
+ if (a == null || !a.trim()) return null;
2006
+ const n = Number(a.trim());
2007
+ if (Number.isInteger(n) && n >= 1 && n <= shown.length)
2008
+ return shown[n - 1].value;
2009
+ const q = a.trim().toLowerCase();
2010
+ const next = options.filter(
2011
+ (o) => `${o.label} ${o.meta || ""} ${o.value}`.toLowerCase().includes(q)
2012
+ );
2013
+ shown = next.length ? next : options;
2014
+ }
2015
+ }
2016
+ /** Note the program once it exists, for the key handler. */
2017
+ attach(p) {
2018
+ this.program = p;
2019
+ }
2020
+ /** Esc during a parked state machine: stop it at the next tick. */
2021
+ requestStop() {
2022
+ this.stopRequested = true;
2023
+ }
2024
+ sleep(ms) {
2025
+ return new Promise((r) => setTimeout(r, ms));
2026
+ }
2027
+ loadExec(sgbd) {
2028
+ return this.R.irLiveExec(sgbd);
2029
+ }
2030
+ route() {
2031
+ }
2032
+ status(_p, text) {
2033
+ this.statusText = text;
2034
+ this.drawFooter();
2035
+ }
2036
+ error(_p, text) {
2037
+ this.statusText = `error: ${text}`;
2038
+ this.drawFooter();
2039
+ }
2040
+ async message(title, body) {
2041
+ await this.term.readLine(
2042
+ `
2043
+ ${title}${body ? `
2044
+ ${body}` : ""}
2045
+ [Enter to continue] `
2046
+ );
2047
+ }
2048
+ /**
2049
+ * INPA's prompts, answered on the keyboard with the meanings irAskInput
2050
+ * gives them: inputdigital is the script's two words and stores 1 for the
2051
+ * true word; the plain OK/Cancel box stores 0 for OK; inputhex takes a hex
2052
+ * string, input2text words, inputnum a real, the rest whole numbers within
2053
+ * the declared range. A cancel (empty answer, Esc) abandons the key.
2054
+ * @param step - the input suspension
2055
+ * @param label - the key's caption, when the script gave no title
2056
+ * @returns what resume() stores, or null for cancel
2057
+ */
2058
+ async askInput(step, label) {
2059
+ const prompts = step.prompts || [];
2060
+ const refs = Math.max(1, Number(step.refs || 1));
2061
+ const name = String(step.name || "");
2062
+ const p0 = prompts[0] || label || "";
2063
+ const p1 = prompts[1] || "";
2064
+ if (name === "inputdigital") {
2065
+ const f = prompts[prompts.length - 2] || "OFF";
2066
+ const t = prompts[prompts.length - 1] || "ON";
2067
+ const a = await this.term.readLine(
2068
+ `
2069
+ ${p0}
2070
+ ${p1}
2071
+ [${t} = y, ${f} = n, cancel = Enter] `
2072
+ );
2073
+ if (a == null || !a.trim()) return null;
2074
+ return /^y/i.test(a.trim()) ? 1 : 0;
2075
+ }
2076
+ if (name === "builtin_3f" && prompts.length <= 2 && refs === 1) {
2077
+ const a = await this.term.readLine(
2078
+ `
2079
+ ${p0}
2080
+ ${p1}
2081
+ [OK = y, cancel = n] `
2082
+ );
2083
+ return a != null && /^y/i.test(a.trim()) ? 0 : null;
2084
+ }
2085
+ const hex = /hex/i.test(name);
2086
+ const text = /text/i.test(name);
2087
+ const real = name === "inputnum";
2088
+ const vals = [];
2089
+ for (let k = 0; k < refs; k++) {
2090
+ const cap = refs > 1 ? prompts[2 + k] || `${p0} (${k + 1}/${refs})` : p1;
2091
+ const range = step.lo != null && step.hi != null && !hex ? ` [${step.lo}..${step.hi}]` : "";
2092
+ const a = await this.term.readLine(`
2093
+ ${p0}
2094
+ ${cap}${range}: `);
2095
+ if (a == null) return null;
2096
+ if (text) {
2097
+ vals.push(String(a));
2098
+ continue;
2099
+ }
2100
+ if (!a.trim()) return null;
2101
+ if (hex) {
2102
+ vals.push(a.trim());
2103
+ continue;
2104
+ }
2105
+ const n = real ? Number(a) : Math.trunc(Number(a));
2106
+ if (!Number.isFinite(n)) return null;
2107
+ if (refs === 1 && step.lo != null && step.hi != null && (n < step.lo || n > step.hi))
2108
+ return null;
2109
+ vals.push(n);
2110
+ }
2111
+ return refs > 1 ? vals : vals[0];
2112
+ }
2113
+ async confirmKey(_p, it, jobs, writes) {
2114
+ const a = await this.term.readLine(
2115
+ `
2116
+ Run "${it.label || it.legendLabel || `F${it.nr}`}"? It can send ${jobs.join(", ")} and ${writes.join(", ")} write${writes.length === 1 ? "s" : ""} to the module. [y/N] `
2117
+ );
2118
+ return !!a && /^y(es)?$/i.test(a.trim());
2119
+ }
2120
+ async confirmWrite(_p, job, arg, ctx) {
2121
+ const every = String(ctx.scope || "").startsWith("screen:") ? " This screen sends it on every refresh; yes allows it while the screen is open." : "";
2122
+ const a = await this.term.readLine(
2123
+ `
2124
+ Send ${job}${arg ? ` ${arg}` : ""} to the module (${ctx.label})?${every} [y/N] `
2125
+ );
2126
+ return !!a && /^y(es)?$/i.test(a.trim());
2127
+ }
2128
+ /**
2129
+ * INPA's togglelist: the active screen's LINE declarations, numbered;
2130
+ * one is picked and switched on or off (a tick list with the multiple
2131
+ * flag, every picked key ';'-joined).
2132
+ */
2133
+ async pickComponent(p, step) {
2134
+ const rows = this.R.ipoScreenComponents(p.exec, p.screen).map((l, i2) => ({
2135
+ key: step.argnum ? String(i2 + 1) : String(l.keys).split(";")[0] || "",
2136
+ caption: l.label || String(l.keys).split(";")[0] || ""
2137
+ }));
2138
+ if (!rows.length) {
2139
+ await this.message(
2140
+ "No components to pick",
2141
+ "This screen lists no components."
2142
+ );
2143
+ return null;
2144
+ }
2145
+ const list = rows.map((r, i2) => ` ${i2 + 1}. ${r.caption} (${r.key})`).join("\n");
2146
+ if (step.multiple) {
2147
+ const a2 = await this.term.readLine(
2148
+ `
2149
+ ${list}
2150
+ Components, comma-separated (Enter cancels): `
2151
+ );
2152
+ const picked = pickNumbers(a2, rows.length).map(
2153
+ (i2) => rows[i2]
2154
+ );
2155
+ if (!picked.length) return null;
2156
+ return { ort: picked.map((r) => r.key).join(";"), ein: 0 };
2157
+ }
2158
+ const a = await this.term.readLine(
2159
+ `
2160
+ ${list}
2161
+ Component number (Enter cancels): `
2162
+ );
2163
+ const [i] = pickNumbers(a, rows.length);
2164
+ if (i == null) return null;
2165
+ const onOff = await this.term.readLine(`On or off? [on/off] `);
2166
+ if (onOff == null || !onOff.trim()) return null;
2167
+ return {
2168
+ ort: rows[i].key,
2169
+ ein: /^on/i.test(onOff.trim()) ? 0 : 1
2170
+ };
2171
+ }
2172
+ /** INPA's Select: which named logical lines to keep on screen. */
2173
+ async pickLines(_p, names, multiple, _current, hints) {
2174
+ if (!names.length) {
2175
+ const where = (hints || []).map((h) => `${h.key} ${h.label} (${h.lines} lines)`).join(", ");
2176
+ await this.message(
2177
+ "Nothing to select here",
2178
+ where ? `Select works after one of: ${where}` : "This screen has no named lines."
2179
+ );
2180
+ return null;
2181
+ }
2182
+ const list = names.map((n, i) => ` ${i + 1}. ${n}`).join("\n");
2183
+ const a = await this.term.readLine(
2184
+ `
2185
+ ${list}
2186
+ Lines to show${multiple ? ", comma-separated" : ""} (a = all, Enter cancels): `
2187
+ );
2188
+ if (a == null) return null;
2189
+ if (/^a(ll)?$/i.test(a.trim())) return [];
2190
+ const picked = pickNumbers(a, names.length).map((i) => names[i]);
2191
+ return picked.length ? picked : null;
2192
+ }
2193
+ /** INPA's save-as dialog: a file name, written when the body ends. */
2194
+ async saveFile() {
2195
+ const a = await this.term.readLine(`
2196
+ Save as [fault-memory.txt]: `);
2197
+ if (a == null) return null;
2198
+ return { name: a.trim() || "fault-memory.txt" };
2199
+ }
2200
+ async writeFile(_p, picked, lines) {
2201
+ writeFileSync4(picked.name, lines.join("\r\n") + "\r\n", "latin1");
2202
+ this.status(_p, `wrote ${picked.name}`);
2203
+ }
2204
+ printScreen(p) {
2205
+ this.term.write(`
2206
+ ${this.gridLines(p).join("\n")}
2207
+ `);
2208
+ }
2209
+ /**
2210
+ * The module a scriptchange names. From the home script it is the car's
2211
+ * own config row for the picked chassis (home.js ipoHomeEcuFor), prepared
2212
+ * the way opening it from the module list prepares it; from any other
2213
+ * script it is the module the car identifies (open.js).
2214
+ */
2215
+ async resolveScriptEcu(from, script, exec) {
2216
+ if (from && from.sgbd === this.R.IPO_HOME_SGBD) {
2217
+ const row = await this.R.ipoHomeEcuFor(from, script);
2218
+ if (!row) return null;
2219
+ try {
2220
+ const api = runtimeGlobals().api;
2221
+ return (await prepareModule(row, api)).ecu;
2222
+ } catch {
2223
+ return row;
2224
+ }
2225
+ }
2226
+ return this.R.ipoResolveScriptEcu(from, script, exec);
2227
+ }
2228
+ /** A %STATE park: tick after IPO_TICK_MS, or stop when Esc was pressed. */
2229
+ async machineTick(_p, step) {
2230
+ this.progressText = `state ${String(step.name || "").replace(/^%/, "")} (Esc stops)`;
2231
+ this.drawFooter();
2232
+ await this.sleep(this.R.IPO_TICK_MS);
2233
+ if (this.stopRequested) {
2234
+ this.stopRequested = false;
2235
+ this.progressText = "";
2236
+ return "stop";
2237
+ }
2238
+ return "tick";
2239
+ }
2240
+ userbox(_p, box) {
2241
+ if (!box) {
2242
+ this.progressText = "";
2243
+ } else {
2244
+ const lines = box.lines || [];
2245
+ const text = lines.length ? String(lines[lines.length - 1]) : "";
2246
+ this.progressText = `${box.title ? `${box.title}: ` : ""}${text}`.trim();
2247
+ }
2248
+ this.drawFooter();
2249
+ }
2250
+ /** The F-key bar: the write verdict per key marks what will ask. */
2251
+ renderKeys(p) {
2252
+ this.writeKeys = /* @__PURE__ */ new Set();
2253
+ const jobsOf = runtimeGlobals().irItemBodyJobs;
2254
+ for (const it of p.items) {
2255
+ const jobs = jobsOf ? jobsOf(p.exec, p.exec.procs[p.menu || ""], it.start, it.end) : [];
2256
+ if (jobs.some((j) => this.R.ipoNeedsConfirm(j)))
2257
+ this.writeKeys.add(it.nr);
2258
+ }
2259
+ this.paint(p);
2260
+ }
2261
+ /** Redraw everything: title, grid, keys, footer. */
2262
+ paint(p) {
2263
+ const w = this.term.columns;
2264
+ const out = [];
2265
+ const title = `${p.ecu.label || p.ecu.sgbd} ${p.ecu.sgbd}.prg ${p.title || ""}`.trim();
2266
+ out.push(title.slice(0, w));
2267
+ out.push("-".repeat(Math.min(w, 78)));
2268
+ if (p.view) {
2269
+ out.push(...(p.view.lines || []).slice(0, this.term.rows - 8));
2270
+ } else {
2271
+ out.push(...this.gridLines(p));
2272
+ }
2273
+ out.push("");
2274
+ out.push(...this.keyLines(p));
2275
+ this.term.write(`\x1B[H\x1B[2J${out.join("\n")}
2276
+ `);
2277
+ this.drawFooter();
2278
+ }
2279
+ /**
2280
+ * The grid as text rows: cells placed at their column, a lamp as a dot
2281
+ * and its word, a gauge as a bar and its number, the rest as its text.
2282
+ */
2283
+ gridLines(p) {
2284
+ const w = this.term.columns;
2285
+ const rows = /* @__PURE__ */ new Map();
2286
+ for (const c of p.cells.values()) {
2287
+ if (!rows.has(c.row)) rows.set(c.row, []);
2288
+ rows.get(c.row).push(c);
2289
+ }
2290
+ const maxRow = rows.size ? Math.max(...rows.keys()) : -1;
2291
+ const out = [];
2292
+ for (let r = 0; r <= maxRow; r++) {
2293
+ const line = [];
2294
+ for (const c of (rows.get(r) || []).sort((a, b) => a.col - b.col)) {
2295
+ const text = cellText(c);
2296
+ while (line.length < c.col) line.push(" ");
2297
+ for (const ch of text) line.push(ch);
2298
+ }
2299
+ out.push(line.join("").replace(/\s+$/, "").slice(0, w));
2300
+ }
2301
+ return out;
2302
+ }
2303
+ /** The key bar: the plain bank on one line, the shifted on the next. */
2304
+ keyLines(p) {
2305
+ const shown = p.items.filter((it) => !it.hidden || !!it.legendLabel);
2306
+ const label = (it) => `${it.shift ? "S" : ""}F${it.shift ? it.nr - 10 : it.nr} ${it.label || it.legendLabel || ""}${this.writeKeys.has(it.nr) ? "*" : ""}`;
2307
+ const plain = shown.filter((it) => !it.shift).map(label);
2308
+ const shifted = shown.filter((it) => it.shift).map(label);
2309
+ const out = [];
2310
+ if (plain.length) out.push(plain.join(" "));
2311
+ if (shifted.length) out.push(shifted.join(" "));
2312
+ out.push(
2313
+ "(1..9,0 = F1..F10, shifted symbols = Shift+F, Esc = back, q = quit, * asks first)"
2314
+ );
2315
+ return out;
2316
+ }
2317
+ /** The two bottom lines: status and progress. */
2318
+ drawFooter() {
2319
+ this.term.write(
2320
+ `\x1B[s\x1B[${Math.max(1, this.term.rows - 1)};1H\x1B[K${this.statusText}
2321
+ \x1B[K${this.progressText}\x1B[u`
2322
+ );
2323
+ }
2324
+ left() {
2325
+ if (this.leftResolve) this.leftResolve();
2326
+ }
2327
+ };
2328
+ function pickNumbers(a, n) {
2329
+ if (a == null) return [];
2330
+ return a.split(/[,\s]+/).map((s) => Number(s)).filter((i) => Number.isInteger(i) && i >= 1 && i <= n).map((i) => i - 1);
2331
+ }
2332
+ function cellText(c) {
2333
+ const text = String(c.text || "");
2334
+ if (c.kind === "lamp") {
2335
+ const word = text.trim();
2336
+ const on = c.meta && c.meta.on != null && word === String(c.meta.on) || /^(1|ein|on|an|ja|yes|aktiv|true)$/i.test(word);
2337
+ return `${on ? "(*)" : "( )"} ${word}`;
2338
+ }
2339
+ if (c.kind === "gauge" && c.meta && c.meta.min != null && c.meta.max != null) {
2340
+ const n = parseFloat(text);
2341
+ if (Number.isFinite(n)) {
2342
+ const span = c.meta.max - c.meta.min || 1;
2343
+ const fill = Math.round(
2344
+ Math.max(0, Math.min(1, (n - c.meta.min) / span)) * GAUGE_WIDTH
2345
+ );
2346
+ return `[${"#".repeat(fill)}${".".repeat(GAUGE_WIDTH - fill)}] ${text.trim()}`;
2347
+ }
2348
+ }
2349
+ return text;
2350
+ }
2351
+ async function tuiCommand(chassis, sgbd, opts = {}) {
2352
+ const R = loadRuntime();
2353
+ const home = !chassis && !sgbd;
2354
+ if (opts.apiFn) setApiImpl(opts.apiFn);
2355
+ let cable = false;
2356
+ if (!opts.noBus) {
2357
+ if (home) {
2358
+ try {
2359
+ await connectBus(opts);
2360
+ cable = true;
2361
+ } catch (e) {
2362
+ if (!(e instanceof CliError)) throw e;
2363
+ }
2364
+ } else {
2365
+ await connectBus(opts);
2366
+ cable = true;
2367
+ }
2368
+ }
2369
+ const api = opts.apiFn || runtimeGlobals().api;
2370
+ const host = terminalHomeHost(
2371
+ api,
2372
+ () => `bmweb-cli ${opts.version || ""} ${cable || opts.noBus && !home ? "cable connected" : "no cable"}`.trim()
2373
+ );
2374
+ Object.assign(R.IPO_HOME_HOST, host);
2375
+ const term = opts.term || nodeTerminal();
2376
+ const ui = new TuiUi(term, R, host);
2377
+ let p = null;
2378
+ try {
2379
+ let ecu;
2380
+ let exec;
2381
+ if (opts.exec) {
2382
+ ecu = opts.ecu || {
2383
+ sgbd: String(sgbd || "").toLowerCase(),
2384
+ label: String(sgbd || "").toUpperCase(),
2385
+ chassis: String(chassis || "").toUpperCase()
2386
+ };
2387
+ exec = opts.exec;
2388
+ } else if (home) {
2389
+ exec = R.ipoHomeExec();
2390
+ ecu = {
2391
+ sgbd: R.IPO_HOME_SGBD,
2392
+ code: "BMWEB",
2393
+ label: "BMWeb",
2394
+ _variant: R.IPO_HOME_SGBD.toUpperCase()
2395
+ };
2396
+ } else {
2397
+ ({ ecu, exec } = await openModule(
2398
+ chassis,
2399
+ sgbd,
2400
+ opts.apiFn
2401
+ ));
2402
+ }
2403
+ p = new R.IpoProgram(ecu, exec, ui);
2404
+ ui.attach(p);
2405
+ const r = await p.start();
2406
+ if (p.noCable) throw new CliError("no cable connected");
2407
+ if (!r.ok) {
2408
+ const last = p.messages[p.messages.length - 1];
2409
+ throw new CliError(
2410
+ `${ecu.sgbd}: the script did not start (${r.reason || "stopped"})` + (last ? `: ${last.title}${last.body ? ` ${last.body}` : ""}` : "")
2411
+ );
2412
+ }
2413
+ if (opts.menu) {
2414
+ if (!exec.procs[opts.menu])
2415
+ throw new CliError(`${ecu.sgbd}: no menu ${opts.menu}`);
2416
+ if (opts.menu !== p.menu) await p.openMenu(opts.menu);
2417
+ }
2418
+ const program = p;
2419
+ const unsubscribe = term.onKey((k) => {
2420
+ const what = keyToPress(k);
2421
+ if (what === null) return;
2422
+ if (what === "quit") {
2423
+ unsubscribe();
2424
+ program.leaveModule().catch(() => {
2425
+ });
2426
+ return;
2427
+ }
2428
+ if (what === "back") {
2429
+ ui.requestStop();
2430
+ program.back().catch(() => {
2431
+ });
2432
+ return;
2433
+ }
2434
+ program.press(what).catch(() => {
2435
+ });
2436
+ });
2437
+ await ui.leftPromise;
2438
+ unsubscribe();
2439
+ return { log: program.log, messages: program.messages };
2440
+ } finally {
2441
+ if (p && !p.closed) p.close();
2442
+ term.close();
2443
+ if (opts.apiFn) setApiImpl(null);
2444
+ if (!opts.noBus) await disconnectBus(R);
2445
+ }
2446
+ }
2447
+
2448
+ // src/bmweb.ts
2449
+ var VERSION = true ? "0.1.0" : "0.0.0-dev";
2450
+ var INCLUDE = {
2451
+ include: {
2452
+ kind: "list",
2453
+ alias: "I",
2454
+ help: "a directory holding the INPA headers a source needs (repeatable)"
2455
+ }
2456
+ };
2457
+ var JSON_FLAG = {
2458
+ json: {
2459
+ kind: "bool",
2460
+ help: "print machine-readable JSON instead of a table"
2461
+ }
2462
+ };
2463
+ var LIVE = {
2464
+ port: {
2465
+ kind: "string",
2466
+ alias: "p",
2467
+ help: "the serial device (the single candidate when there is one)"
2468
+ },
2469
+ api: {
2470
+ kind: "string",
2471
+ help: "the site the module data comes from (default https://bmweb.danner.ink/)"
2472
+ },
2473
+ refresh: {
2474
+ kind: "bool",
2475
+ help: "fetch the module data again even when the cached copy is fresh"
2476
+ }
2477
+ };
2478
+ var COMMANDS = {
2479
+ "ipo info": {
2480
+ usage: "bmweb ipo info <file.IPO|file.IPS|file.SRC> [-I dir]... [--json]",
2481
+ summary: "what a script is: form, includes, entry, procedures, DLL imports, and every menu with its keys and jobs",
2482
+ flags: { ...INCLUDE, ...JSON_FLAG },
2483
+ async run(pos, flags) {
2484
+ const file = pos[0];
2485
+ if (!file || pos.length > 1)
2486
+ throw new CliError("usage: " + COMMANDS["ipo info"].usage);
2487
+ return ipoInfo(file, flags.include || [], !!flags.json);
2488
+ }
2489
+ },
2490
+ "ipo keys": {
2491
+ usage: "bmweb ipo keys <file.IPO|file.IPS|file.SRC> [--menu m_x] [-I dir]... [--json]",
2492
+ summary: "the F-keys of every menu (or one): the screen each opens, the jobs it sends, and which of them write",
2493
+ flags: {
2494
+ ...INCLUDE,
2495
+ menu: { kind: "string", alias: "m", help: "only this menu procedure" },
2496
+ ...JSON_FLAG
2497
+ },
2498
+ async run(pos, flags) {
2499
+ const file = pos[0];
2500
+ if (!file || pos.length > 1)
2501
+ throw new CliError("usage: " + COMMANDS["ipo keys"].usage);
2502
+ return ipoKeys(
2503
+ file,
2504
+ flags.include || [],
2505
+ flags.menu,
2506
+ !!flags.json
2507
+ );
2508
+ }
2509
+ },
2510
+ "ipo compile": {
2511
+ usage: "bmweb ipo compile <file.IPS|file.SRC> [-I dir]... [-o out.ipoexec.json]",
2512
+ summary: "compile an INPA source into the app's exec form (JSON); missing includes are named",
2513
+ flags: {
2514
+ ...INCLUDE,
2515
+ out: {
2516
+ kind: "string",
2517
+ alias: "o",
2518
+ help: "where to write (default: <stem>.ipoexec.json beside the source)"
2519
+ }
2520
+ },
2521
+ async run(pos, flags) {
2522
+ const file = pos[0];
2523
+ if (!file || pos.length > 1)
2524
+ throw new CliError(
2525
+ "usage: " + COMMANDS["ipo compile"].usage
2526
+ );
2527
+ return ipoCompile(
2528
+ file,
2529
+ flags.include || [],
2530
+ flags.out
2531
+ );
2532
+ }
2533
+ },
2534
+ search: {
2535
+ usage: "bmweb search <query...> [--chassis E46] [--limit N] [--refresh] [--json]",
2536
+ summary: "find the INPA key or screen that does a thing, across every module the site ships, with a deep link per hit",
2537
+ flags: {
2538
+ chassis: {
2539
+ kind: "string",
2540
+ alias: "c",
2541
+ help: "only modules this chassis carries"
2542
+ },
2543
+ limit: { kind: "number", alias: "n", help: "rows to show (default 50)" },
2544
+ refresh: {
2545
+ kind: "bool",
2546
+ help: "fetch the index again even when the cached copy is fresh"
2547
+ },
2548
+ ...JSON_FLAG
2549
+ },
2550
+ async run(pos, flags) {
2551
+ const query = pos.join(" ").trim();
2552
+ if (!query)
2553
+ throw new CliError("usage: " + COMMANDS.search.usage);
2554
+ const index = await loadIndex({ refresh: !!flags.refresh });
2555
+ return runSearch(query, {
2556
+ chassis: flags.chassis,
2557
+ limit: flags.limit || 50,
2558
+ json: !!flags.json,
2559
+ index
2560
+ });
2561
+ }
2562
+ },
2563
+ "report show": {
2564
+ usage: "bmweb report show <link-or-payload> [--json]",
2565
+ summary: "decode a shared Garage report link: module, code, text, count per fault, plus the silent addresses",
2566
+ flags: { ...JSON_FLAG },
2567
+ async run(pos, flags) {
2568
+ const arg = pos[0];
2569
+ if (!arg || pos.length > 1)
2570
+ throw new CliError(
2571
+ "usage: " + COMMANDS["report show"].usage
2572
+ );
2573
+ return reportShow(arg, !!flags.json);
2574
+ }
2575
+ },
2576
+ "report diff": {
2577
+ usage: "bmweb report diff <link-a> <link-b> [--json]",
2578
+ summary: "what changed between two shared reports: new, cleared and still-present faults per module",
2579
+ flags: { ...JSON_FLAG },
2580
+ async run(pos, flags) {
2581
+ const [a, b] = pos;
2582
+ if (!a || !b || pos.length > 2)
2583
+ throw new CliError(
2584
+ "usage: " + COMMANDS["report diff"].usage
2585
+ );
2586
+ return reportDiff(a, b, !!flags.json);
2587
+ }
2588
+ },
2589
+ ports: {
2590
+ usage: "bmweb ports [--json]",
2591
+ summary: "the serial ports a K+DCAN cable shows up as (cu.usbserial*, cu.SLAB*, cu.wchusbserial*, ttyUSB*, ttyACM*)",
2592
+ flags: { ...JSON_FLAG },
2593
+ async run(pos, flags) {
2594
+ if (pos.length)
2595
+ throw new CliError("usage: " + COMMANDS.ports.usage);
2596
+ return portsCommand(!!flags.json);
2597
+ }
2598
+ },
2599
+ job: {
2600
+ usage: "bmweb job <sgbd> <JOB> [arg] [--port p] [--api url] [--yes] [--json]",
2601
+ summary: "one raw job on one module over the cable, like the app's Tool32; a write needs --yes or a y answer",
2602
+ flags: {
2603
+ ...LIVE,
2604
+ yes: {
2605
+ kind: "bool",
2606
+ alias: "y",
2607
+ help: "consent to a write job on the command line"
2608
+ },
2609
+ ...JSON_FLAG
2610
+ },
2611
+ async run(pos, flags) {
2612
+ const [sgbd, job, arg] = pos;
2613
+ if (!sgbd || !job || pos.length > 3)
2614
+ throw new CliError("usage: " + COMMANDS.job.usage);
2615
+ const { R } = await connectBus(liveOptions(flags));
2616
+ try {
2617
+ return await jobCommand(sgbd, job, {
2618
+ arg,
2619
+ yes: !!flags.yes,
2620
+ json: !!flags.json
2621
+ });
2622
+ } finally {
2623
+ await disconnectBus(R);
2624
+ }
2625
+ }
2626
+ },
2627
+ scan: {
2628
+ usage: "bmweb scan <chassis> [--port p] [--api url] [--share] [--json]",
2629
+ summary: `INPA's whole-vehicle script over the cable (${SCAN_CHASSIS.join(" ")}): every fault memory as a report, --share adds a Garage link`,
2630
+ flags: {
2631
+ ...LIVE,
2632
+ share: {
2633
+ kind: "bool",
2634
+ help: "print a Garage share link carrying the report"
2635
+ },
2636
+ label: {
2637
+ kind: "string",
2638
+ help: "the car name on the share link"
2639
+ },
2640
+ ...JSON_FLAG
2641
+ },
2642
+ async run(pos, flags) {
2643
+ const chassis = pos[0];
2644
+ if (!chassis || pos.length > 1)
2645
+ throw new CliError("usage: " + COMMANDS.scan.usage);
2646
+ const { R } = await connectBus(liveOptions(flags));
2647
+ try {
2648
+ const r = await scanCommand(chassis, {
2649
+ share: !!flags.share,
2650
+ json: !!flags.json,
2651
+ label: flags.label
2652
+ });
2653
+ return r.lines;
2654
+ } finally {
2655
+ await disconnectBus(R);
2656
+ }
2657
+ }
2658
+ },
2659
+ tui: {
2660
+ usage: "bmweb tui [<chassis> <sgbd>] [--port p] [--api url] [--menu m_x]",
2661
+ summary: "INPA screens in the terminal: the app's home (pick a chassis and a module) with no arguments, else that module; F-keys on the number row, every write asked first, released on quit",
2662
+ flags: {
2663
+ ...LIVE,
2664
+ menu: {
2665
+ kind: "string",
2666
+ alias: "m",
2667
+ help: "the menu procedure to open once the script is up"
2668
+ }
2669
+ },
2670
+ async run(pos, flags) {
2671
+ const [chassis, sgbd] = pos;
2672
+ if (pos.length === 1 || pos.length > 2)
2673
+ throw new CliError("usage: " + COMMANDS.tui.usage);
2674
+ const r = await tuiCommand(chassis, sgbd, {
2675
+ ...liveOptions(flags),
2676
+ menu: flags.menu,
2677
+ version: VERSION
2678
+ });
2679
+ return [
2680
+ `${r.log.length} job${r.log.length === 1 ? "" : "s"} sent:`,
2681
+ ...r.log.map(
2682
+ (l) => ` ${l.target} ${l.job}${l.arg ? ` ${l.arg}` : ""} ${l.status}`
2683
+ )
2684
+ ];
2685
+ }
2686
+ }
2687
+ };
2688
+ function liveOptions(flags) {
2689
+ return {
2690
+ port: flags.port,
2691
+ api: flags.api,
2692
+ refresh: !!flags.refresh
2693
+ };
2694
+ }
2695
+ function helpText() {
2696
+ const out = [
2697
+ `bmweb ${VERSION}: BMWeb's tools as a command line (https://bmweb.danner.ink/)`,
2698
+ "",
2699
+ "usage: bmweb <command> [options]",
2700
+ "",
2701
+ "commands:"
2702
+ ];
2703
+ const names = Object.keys(COMMANDS);
2704
+ const w = Math.max(...names.map((n) => n.length));
2705
+ for (const n of names)
2706
+ out.push(` ${n.padEnd(w)} ${COMMANDS[n].summary}`);
2707
+ out.push(
2708
+ "",
2709
+ " bmweb <command> --help options of one command",
2710
+ " bmweb --version",
2711
+ "",
2712
+ "ports, job, scan and tui talk to the car over a K+DCAN cable (the serialport",
2713
+ "package, an optional dependency). Module data and the search index are",
2714
+ "fetched from the site and cached under $XDG_CACHE_HOME/bmweb-cli",
2715
+ "(default ~/.cache/bmweb-cli) for a day; nothing BMW-derived ships here."
2716
+ );
2717
+ return out;
2718
+ }
2719
+ function commandHelp(name) {
2720
+ const c = COMMANDS[name];
2721
+ return [
2722
+ `usage: ${c.usage}`,
2723
+ "",
2724
+ c.summary,
2725
+ "",
2726
+ "options:",
2727
+ ...helpLines(c.flags)
2728
+ ];
2729
+ }
2730
+ function resolveCommand(argv) {
2731
+ for (const n of [2, 1]) {
2732
+ const name = argv.slice(0, n).join(" ");
2733
+ if (COMMANDS[name]) return { name, rest: argv.slice(n) };
2734
+ }
2735
+ return null;
2736
+ }
2737
+ async function main(argv, out = (l) => process.stdout.write(l + "\n"), err = (l) => process.stderr.write(l + "\n")) {
2738
+ try {
2739
+ if (!argv.length || argv[0] === "--help" || argv[0] === "-h" || argv[0] === "help") {
2740
+ helpText().forEach(out);
2741
+ return 0;
2742
+ }
2743
+ if (argv[0] === "--version" || argv[0] === "-v") {
2744
+ out(VERSION);
2745
+ return 0;
2746
+ }
2747
+ const picked = resolveCommand(argv);
2748
+ if (!picked) {
2749
+ const group = argv[0];
2750
+ const members = Object.keys(COMMANDS).filter(
2751
+ (n) => n.startsWith(group + " ")
2752
+ );
2753
+ if (members.length) {
2754
+ members.forEach((n) => out(`usage: ${COMMANDS[n].usage}`));
2755
+ return 1;
2756
+ }
2757
+ throw new CliError(
2758
+ `unknown command "${argv.join(" ")}" (try bmweb --help)`
2759
+ );
2760
+ }
2761
+ if (picked.rest.includes("--help") || picked.rest.includes("-h")) {
2762
+ commandHelp(picked.name).forEach(out);
2763
+ return 0;
2764
+ }
2765
+ const c = COMMANDS[picked.name];
2766
+ const parsed = parseArgs(picked.rest, c.flags);
2767
+ const lines = await c.run(parsed.positional, parsed.flags);
2768
+ lines.forEach(out);
2769
+ return 0;
2770
+ } catch (e) {
2771
+ if (e instanceof CliError) {
2772
+ err(`bmweb: ${e.message}`);
2773
+ return 1;
2774
+ }
2775
+ err(`bmweb: unexpected error: ${e.stack || e}`);
2776
+ return 1;
2777
+ }
2778
+ }
2779
+ if (typeof process !== "undefined" && process.argv[1] && /bmweb(\.js)?$/.test(process.argv[1])) {
2780
+ main(process.argv.slice(2)).then((code) => {
2781
+ process.exitCode = code;
2782
+ });
2783
+ }
2784
+ export {
2785
+ VERSION,
2786
+ commandHelp,
2787
+ helpText,
2788
+ main,
2789
+ resolveCommand
2790
+ };