@takodotid/circuit 0.26.2 → 0.28.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.
package/dist/cli/index.js CHANGED
@@ -110,6 +110,12 @@ var MARKER = /<secret:([A-Za-z0-9_./:-]+)>/g;
110
110
  var substitute = (text) => text.replace(MARKER, (_, reference) => valueOf(reference));
111
111
  var valueOfReference = valueOf;
112
112
 
113
+ // src/transport/console.ts
114
+ import { spawn } from "node:child_process";
115
+ import { EventEmitter } from "node:events";
116
+ import { Duplex } from "node:stream";
117
+ import { Client as Client2 } from "ssh2";
118
+
113
119
  // src/transport/ssh.ts
114
120
  import { Client } from "ssh2";
115
121
  function connect(login) {
@@ -202,6 +208,321 @@ function upload(client, path, content) {
202
208
  });
203
209
  }
204
210
 
211
+ // src/transport/console.ts
212
+ var RELAY = `import fcntl, os, select, sys, termios
213
+
214
+ port, baud = sys.argv[1], int(sys.argv[2])
215
+ fd = os.open(port, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
216
+
217
+ try:
218
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
219
+ except OSError:
220
+ sys.stderr.write("busy: another session holds " + port + "\\n")
221
+ sys.exit(3)
222
+
223
+ attrs = termios.tcgetattr(fd)
224
+ attrs[0] = attrs[1] = attrs[3] = 0
225
+ attrs[2] = termios.CS8 | termios.CREAD | termios.CLOCAL
226
+ attrs[4] = attrs[5] = getattr(termios, "B%d" % baud)
227
+ termios.tcsetattr(fd, termios.TCSANOW, attrs)
228
+ termios.tcflush(fd, termios.TCIOFLUSH)
229
+
230
+ sys.stderr.write("ready\\n")
231
+ sys.stderr.flush()
232
+
233
+ while True:
234
+ ready, _, _ = select.select([0, fd], [], [])
235
+
236
+ if 0 in ready:
237
+ data = os.read(0, 4096)
238
+ if not data:
239
+ break
240
+ while data:
241
+ try:
242
+ data = data[os.write(fd, data):]
243
+ except BlockingIOError:
244
+ select.select([], [fd], [])
245
+
246
+ if fd in ready:
247
+ try:
248
+ data = os.read(fd, 4096)
249
+ except BlockingIOError:
250
+ continue
251
+ if data:
252
+ os.write(1, data)
253
+ `;
254
+ var RELAY_PATH = "/tmp/circuit-console.py";
255
+ var PORT = /^\/dev\/[\w./:+-]+$/;
256
+ var WINDOWS_PORT = /^COM\d+$/i;
257
+ var WINDOWS_RELAY = `
258
+ $port = New-Object System.IO.Ports.SerialPort $env:CIRCUIT_PORT, ([int]$env:CIRCUIT_BAUD), 'None', 8, 'One'
259
+ try { $port.Open() } catch [System.UnauthorizedAccessException] { [Console]::Error.WriteLine("busy: another program holds " + $env:CIRCUIT_PORT); exit 3 } catch { [Console]::Error.WriteLine("cannot open " + $env:CIRCUIT_PORT + ": " + $_.Exception.Message); exit 2 }
260
+ [Console]::Error.WriteLine("ready")
261
+ $in = [Console]::OpenStandardInput()
262
+ $out = [Console]::OpenStandardOutput()
263
+ $buffer = New-Object byte[] 4096
264
+ $reading = $in.ReadAsync($buffer, 0, $buffer.Length)
265
+ while ($true) {
266
+ if ($reading.IsCompleted) {
267
+ if ($reading.Result -le 0) { break }
268
+ $port.Write($buffer, 0, $reading.Result)
269
+ $reading = $in.ReadAsync($buffer, 0, $buffer.Length)
270
+ }
271
+ $waiting = $port.BytesToRead
272
+ if ($waiting -gt 0) {
273
+ $data = New-Object byte[] $waiting
274
+ $got = $port.Read($data, 0, $waiting)
275
+ $out.Write($data, 0, $got)
276
+ $out.Flush()
277
+ } else {
278
+ Start-Sleep -Milliseconds 10
279
+ }
280
+ }
281
+ $port.Close()
282
+ `;
283
+ function connectJump(jump) {
284
+ return new Promise((done, fail) => {
285
+ const client = new Client2;
286
+ const login = { username: jump.user, password: resolve(jump.password), readyTimeout: 60000 };
287
+ client.on("ready", () => done(client)).on("error", fail);
288
+ if (!jump.cloudflared) {
289
+ client.connect({ ...login, host: jump.host, port: jump.port ?? 22 });
290
+ return;
291
+ }
292
+ const tunnel = spawn("cloudflared", ["access", "ssh", "--hostname", jump.host], { stdio: ["pipe", "pipe", "ignore"] });
293
+ tunnel.on("error", (error) => fail(new Error(`cloudflared did not start: ${error.message}`)));
294
+ client.on("close", () => tunnel.kill());
295
+ client.connect({ ...login, sock: Duplex.from({ readable: tunnel.stdout, writable: tunnel.stdin }) });
296
+ });
297
+ }
298
+ async function openConsole(console2) {
299
+ const windows = !console2.jump && process.platform === "win32";
300
+ if (windows ? !WINDOWS_PORT.test(console2.port) : !PORT.test(console2.port)) {
301
+ throw new Error(`console port ${console2.port} is not ${windows ? "a COM port" : "a path under /dev"}`);
302
+ }
303
+ if (!Number.isInteger(console2.baud) || console2.baud <= 0)
304
+ throw new Error(`console baud ${console2.baud} is not a line speed`);
305
+ for (let attempt = 1;; attempt++) {
306
+ try {
307
+ return await (console2.jump ? openOnJump(console2, console2.jump) : openHere(console2));
308
+ } catch (error) {
309
+ if (attempt >= 5 || !/busy:/.test(error.message))
310
+ throw error;
311
+ await new Promise((done) => setTimeout(done, 5000));
312
+ }
313
+ }
314
+ }
315
+ async function openOnJump(console2, jump) {
316
+ const client = await connectJump(jump);
317
+ try {
318
+ const encoded = Buffer.from(RELAY).toString("base64");
319
+ await exec(client, `sh -c 'printf %s ${encoded} | base64 -d > ${RELAY_PATH}'`, 30000);
320
+ const channel = await new Promise((done, fail) => {
321
+ client.exec(`python3 ${RELAY_PATH} ${console2.port} ${console2.baud}`, (error, stream) => error ? fail(error) : done(stream));
322
+ });
323
+ await relayReady(channel);
324
+ return {
325
+ channel: asTerminal(channel),
326
+ close: () => {
327
+ channel.end();
328
+ client.end();
329
+ }
330
+ };
331
+ } catch (error) {
332
+ client.end();
333
+ throw error;
334
+ }
335
+ }
336
+ async function openHere(console2) {
337
+ const child = process.platform === "win32" ? spawn("powershell", ["-NoProfile", "-NonInteractive", "-Command", WINDOWS_RELAY], {
338
+ env: { ...process.env, CIRCUIT_PORT: console2.port, CIRCUIT_BAUD: String(console2.baud) }
339
+ }) : spawn("python3", ["-c", RELAY, console2.port, String(console2.baud)]);
340
+ const channel = Object.assign(new EventEmitter, {
341
+ stderr: child.stderr,
342
+ write: (data) => child.stdin.write(data),
343
+ end: () => child.stdin.end()
344
+ });
345
+ child.stdout.on("data", (chunk) => channel.emit("data", chunk));
346
+ child.on("close", () => channel.emit("close"));
347
+ child.on("error", (error) => channel.emit("close", error));
348
+ const asChannel = channel;
349
+ try {
350
+ await relayReady(asChannel);
351
+ } catch (error) {
352
+ child.kill();
353
+ throw error;
354
+ }
355
+ return {
356
+ channel: asTerminal(asChannel),
357
+ close: () => {
358
+ channel.end();
359
+ setTimeout(() => child.kill(), 2000).unref();
360
+ }
361
+ };
362
+ }
363
+ function relayReady(channel) {
364
+ return new Promise((done, fail) => {
365
+ let said = "";
366
+ const limit = setTimeout(() => fail(new Error(`the console relay did not start: ${said.trim()}`)), 30000);
367
+ channel.stderr.on("data", (chunk) => {
368
+ said += chunk.toString("utf8");
369
+ if (said.includes("ready")) {
370
+ clearTimeout(limit);
371
+ done();
372
+ }
373
+ });
374
+ channel.on("close", () => {
375
+ clearTimeout(limit);
376
+ fail(new Error(`the console relay stopped: ${said.trim() || "no reason given"}`));
377
+ });
378
+ });
379
+ }
380
+ function asTerminal(channel) {
381
+ const write = channel.write.bind(channel);
382
+ channel.write = (data, ...rest) => write(typeof data === "string" ? data.replace(/\r?\n/g, "\r") : data, ...rest);
383
+ return channel;
384
+ }
385
+ function consoleOf(device) {
386
+ const console2 = device.connection.console;
387
+ if (!console2)
388
+ throw new Error(`${device.name}: no connection.console`);
389
+ const user = device.users?.[device.connection.user];
390
+ if (!user)
391
+ throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
392
+ return { console: console2, user: device.connection.user, password: resolve(user.password) };
393
+ }
394
+ var ESCAPES = /\u001b(?:\[[0-9;?]*[A-Za-z]|[()][A-Z0-9]|[=>78c])/g;
395
+ function expect(channel, write, patterns, timeout = 30000) {
396
+ return new Promise((done, fail) => {
397
+ let text = "";
398
+ const stop = () => {
399
+ clearTimeout(limit);
400
+ channel.removeListener("data", onData);
401
+ };
402
+ const onData = (chunk) => {
403
+ text += chunk.toString("utf8").replace(/\r/g, "").replace(ESCAPES, "");
404
+ const index = patterns.findIndex((pattern) => pattern.test(text));
405
+ if (index < 0)
406
+ return;
407
+ stop();
408
+ done({ index, text });
409
+ };
410
+ const limit = setTimeout(() => {
411
+ stop();
412
+ fail(new Error(`the console did not answer: ${text.slice(-200)}`));
413
+ }, timeout);
414
+ channel.on("data", onData);
415
+ if (write)
416
+ channel.write(write);
417
+ });
418
+ }
419
+ async function consoleLogin(channel, dialect, user, password) {
420
+ const others = dialect.others ?? [];
421
+ const states = [dialect.login, dialect.password, ...others.map(([pattern]) => pattern), dialect.prompt];
422
+ const toward = (index) => {
423
+ if (index === 1)
424
+ return `
425
+ `;
426
+ if (index < 2 + others.length)
427
+ return others[index - 2][1];
428
+ return `${dialect.logout}
429
+ `;
430
+ };
431
+ let state = (await expect(channel, `
432
+ `, states)).index;
433
+ for (let step = 0;state !== 0; step++) {
434
+ if (step >= 10)
435
+ throw new Error("the console never reached a login prompt");
436
+ state = (await expect(channel, toward(state), states)).index;
437
+ }
438
+ const name = dialect.user ? dialect.user(user) : user;
439
+ if ((await expect(channel, `${name}
440
+ `, [dialect.password, dialect.login, dialect.prompt])).index !== 0) {
441
+ throw new Error("the console did not ask for a password");
442
+ }
443
+ let answer = await expect(channel, `${password}
444
+ `, [dialect.prompt, dialect.login, ...others.map(([pattern]) => pattern)], 60000);
445
+ for (let step = 0;answer.index >= 2; step++) {
446
+ if (step >= 5)
447
+ throw new Error("the console kept asking after the login");
448
+ answer = await expect(channel, others[answer.index - 2][1], [
449
+ dialect.prompt,
450
+ dialect.login,
451
+ ...others.map(([pattern]) => pattern)
452
+ ]);
453
+ }
454
+ if (answer.index === 1)
455
+ throw new Error(`the console refused the login of ${user}`);
456
+ await quiet(channel, 800);
457
+ }
458
+ function quiet(channel, silence) {
459
+ return new Promise((done) => {
460
+ let timer = setTimeout(finish, silence);
461
+ const limit = setTimeout(finish, 5000);
462
+ function finish() {
463
+ clearTimeout(timer);
464
+ clearTimeout(limit);
465
+ channel.removeListener("data", onData);
466
+ done();
467
+ }
468
+ function onData() {
469
+ clearTimeout(timer);
470
+ timer = setTimeout(finish, silence);
471
+ }
472
+ channel.on("data", onData);
473
+ });
474
+ }
475
+ function afterEcho(text, command) {
476
+ for (let start = text.indexOf(command[0] ?? "");start >= 0; start = text.indexOf(command[0], start + 1)) {
477
+ const end = echoEnd(text, command, start);
478
+ if (end !== undefined)
479
+ return text.slice(end).replace(/^\r?\n/, "");
480
+ }
481
+ return text;
482
+ }
483
+ function echoEnd(text, command, start) {
484
+ let at = start;
485
+ for (const character of command) {
486
+ while (at < text.length && text[at] !== character && /[\r\n]/.test(text[at]))
487
+ at++;
488
+ if (text[at] !== character)
489
+ return;
490
+ at++;
491
+ }
492
+ return at;
493
+ }
494
+
495
+ class ConsoleShell {
496
+ channel;
497
+ dialect;
498
+ constructor(channel, dialect) {
499
+ this.channel = channel;
500
+ this.dialect = dialect;
501
+ }
502
+ async run(command, timeout = 120000) {
503
+ const pagers = (this.dialect.others ?? []).filter(([, reply]) => !reply.endsWith(`
504
+ `));
505
+ const patterns = [this.dialect.prompt, ...pagers.map(([pattern]) => pattern)];
506
+ let { index, text } = await expect(this.channel, `${command}
507
+ `, patterns, timeout);
508
+ let output = "";
509
+ while (index > 0) {
510
+ const [pattern, reply] = pagers[index - 1];
511
+ output += text.replace(pattern, "");
512
+ ({ index, text } = await expect(this.channel, reply, patterns, timeout));
513
+ }
514
+ output += text;
515
+ const lines = afterEcho(output, command).split(`
516
+ `);
517
+ return lines.slice(0, -1).join(`
518
+ `);
519
+ }
520
+ logout() {
521
+ this.channel.write(`${this.dialect.logout}
522
+ `);
523
+ }
524
+ }
525
+
205
526
  // src/adapters/devices/fortios/names.ts
206
527
  function nameOf(device, name) {
207
528
  if (device.interfaces && name in device.interfaces)
@@ -1134,7 +1455,9 @@ var TABLES = {
1134
1455
  "firewall policy6": ORDERED,
1135
1456
  "firewall local-in-policy": ORDERED,
1136
1457
  "firewall local-in-policy6": ORDERED,
1137
- "firewall central-snat-map": ORDERED
1458
+ "firewall central-snat-map": ORDERED,
1459
+ "router static": { distinct: true },
1460
+ "router static6": { distinct: true }
1138
1461
  };
1139
1462
  var IGNORED = new Set(["uuid", "snmp-index"]);
1140
1463
  var IGNORED_IN = { "system global": ["cfg-save", "cfg-revert-timeout", "alias"] };
@@ -1271,7 +1594,7 @@ function compare(path, want, have, table, top, context) {
1271
1594
  }
1272
1595
  for (const [header, child] of want.blocks)
1273
1596
  nested(header, child, have.blocks.get(header) ?? new Block);
1274
- const entries = table?.byContent ? byContent(path, want, have) : byId(path, want, have, table, context);
1597
+ const entries = table?.byContent ? byContent(path, want, have) : table?.distinct ? distinctById(path, want, have, table, context) : byId(path, want, have, table, context);
1275
1598
  send.push(...entries.send);
1276
1599
  show.push(...entries.show);
1277
1600
  if (top) {
@@ -1320,6 +1643,30 @@ function byId(path, want, have, table, context) {
1320
1643
  }
1321
1644
  return { send, show, removals, removed };
1322
1645
  }
1646
+ function distinctById(path, want, have, table, context) {
1647
+ const deletes = [];
1648
+ const creates = [];
1649
+ const show = [];
1650
+ for (const [key, entry] of want.entries) {
1651
+ const old = have.entries.get(key);
1652
+ const where = [...path, bare(entry.id)];
1653
+ if (old) {
1654
+ if (!compare(where, entry.block, old.block, undefined, false, context).send.length)
1655
+ continue;
1656
+ deletes.push(`delete ${old.id}`);
1657
+ show.push(...shown("-", where, old.block));
1658
+ }
1659
+ creates.push(...creation(entry, table));
1660
+ show.push(...shown("+", where, entry.block));
1661
+ }
1662
+ for (const [key, entry] of have.entries) {
1663
+ if (want.entries.has(key) || isFactory(table, entry))
1664
+ continue;
1665
+ deletes.push(`delete ${entry.id}`);
1666
+ show.push(...shown("-", [...path, bare(entry.id)], entry.block));
1667
+ }
1668
+ return { send: [...deletes, ...creates], show, removals: [], removed: [] };
1669
+ }
1323
1670
  function moves(want, have) {
1324
1671
  const wanted = [...want.entries.keys()];
1325
1672
  const order = [...have.entries.keys()].filter((key) => want.entries.has(key));
@@ -2356,7 +2703,43 @@ async function login(device) {
2356
2703
  password: resolve(user.password)
2357
2704
  });
2358
2705
  }
2359
- async function session(device, work) {
2706
+ var FORTIOS_CONSOLE = {
2707
+ login: /login: $/,
2708
+ password: /Password: $/,
2709
+ prompt: /[#$] $/,
2710
+ logout: "exit",
2711
+ others: [
2712
+ [BANNER2, "a"],
2713
+ [/--More-- $/, "q"],
2714
+ [/\([^)\n]*\) [#$] $/, `end
2715
+ `]
2716
+ ]
2717
+ };
2718
+ async function consoleLogout(cli) {
2719
+ await leave(cli).catch(() => {
2720
+ return;
2721
+ });
2722
+ await cli.run("exit", 1e4).catch(() => {
2723
+ return;
2724
+ });
2725
+ }
2726
+ async function consoleSession(device, work) {
2727
+ const { console: console2, user, password } = consoleOf(device);
2728
+ const opened = await openConsole(console2);
2729
+ try {
2730
+ await consoleLogin(opened.channel, FORTIOS_CONSOLE, user, password);
2731
+ const cli = new Cli(opened.channel);
2732
+ const relogin = () => consoleLogin(opened.channel, FORTIOS_CONSOLE, user, password);
2733
+ const result = await work(cli, relogin);
2734
+ await consoleLogout(cli);
2735
+ return result;
2736
+ } finally {
2737
+ opened.close();
2738
+ }
2739
+ }
2740
+ async function session(device, work, options = {}) {
2741
+ if (options.console)
2742
+ return consoleSession(device, (cli) => work(cli));
2360
2743
  const client = await login(device);
2361
2744
  try {
2362
2745
  const channel = await shell(client, 4000);
@@ -2412,8 +2795,8 @@ async function readConfig(cli, device) {
2412
2795
  throw new Error(`${device.name} returned no configuration`);
2413
2796
  return scrub(clean(text));
2414
2797
  }
2415
- function read(device) {
2416
- return session(device, (cli) => readConfig(cli, device));
2798
+ function read(device, _network = [], options = {}) {
2799
+ return session(device, (cli) => readConfig(cli, device), options);
2417
2800
  }
2418
2801
  function withSecrets(line) {
2419
2802
  const key = line.startsWith("set private-key ");
@@ -2490,7 +2873,7 @@ async function configure(cli, plan, guard) {
2490
2873
  class Reloaded extends Error {
2491
2874
  }
2492
2875
  var RELOAD = { attempts: 20, interval: 15000 };
2493
- async function settle(device) {
2876
+ async function settle(device, options) {
2494
2877
  for (let attempt = 0;attempt < RELOAD.attempts; attempt++) {
2495
2878
  await new Promise((done) => setTimeout(done, RELOAD.interval));
2496
2879
  const settled = await session(device, async (cli) => {
@@ -2498,18 +2881,26 @@ async function settle(device) {
2498
2881
  return false;
2499
2882
  await saving(cli, "automatic");
2500
2883
  return true;
2501
- }).catch(() => false);
2884
+ }, options).catch(() => false);
2502
2885
  if (settled)
2503
2886
  return "the device is back and saves changes on its own again";
2504
2887
  }
2505
2888
  return "the device did not come back in time and still reverts every change; set `cfg-save automatic` under `config system global`";
2506
2889
  }
2507
2890
  async function apply(device, _network, plan, options) {
2891
+ const seconds = options.rollback * 60;
2508
2892
  try {
2893
+ if (options.console) {
2894
+ await consoleSession(device, async (cli, relogin) => {
2895
+ await checkVersion(cli, device);
2896
+ await configure(cli, plan, { seconds, prove: relogin });
2897
+ });
2898
+ return;
2899
+ }
2509
2900
  await session(device, async (cli) => {
2510
2901
  await checkVersion(cli, device);
2511
2902
  await configure(cli, plan, {
2512
- seconds: options.rollback * 60,
2903
+ seconds,
2513
2904
  prove: async () => {
2514
2905
  const proof = await login(device);
2515
2906
  proof.end();
@@ -2520,7 +2911,7 @@ async function apply(device, _network, plan, options) {
2520
2911
  if (!(error instanceof Reloaded))
2521
2912
  throw error;
2522
2913
  throw new Error(`${error.message}
2523
- ${await settle(device)}`);
2914
+ ${await settle(device, options)}`);
2524
2915
  }
2525
2916
  }
2526
2917
  function fillLines(set, family, prefixes) {
@@ -2920,6 +3311,7 @@ function serviceGaps(device) {
2920
3311
  }
2921
3312
  var fortios = {
2922
3313
  extension: ".conf",
3314
+ console: true,
2923
3315
  unsupported,
2924
3316
  render,
2925
3317
  read,
@@ -4129,8 +4521,8 @@ async function login2(device) {
4129
4521
  password: resolve(user.password)
4130
4522
  });
4131
4523
  }
4132
- async function checkVersion2(client, device) {
4133
- const version = await exec(client, "show version | match Junos:");
4524
+ async function checkVersion2(ask, device) {
4525
+ const version = await ask("show version | match Junos:");
4134
4526
  const release = /Junos: (\d+)\.(\S+)/.exec(version);
4135
4527
  if (!release)
4136
4528
  throw new Error(`${device.name} did not say which Junos it runs: ${version.slice(0, 200)}`);
@@ -4138,10 +4530,19 @@ async function checkVersion2(client, device) {
4138
4530
  throw new Error(`${device.name} runs Junos ${release[1]}.${release[2]}; this adapter renders for Junos ${SUPPORTED_MAJOR}`);
4139
4531
  }
4140
4532
  }
4141
- async function read2(device) {
4533
+ async function read2(device, _network = [], options = {}) {
4534
+ if (options.console) {
4535
+ return onConsole(device, async (channel) => {
4536
+ await checkVersion2((command) => run(channel, command), device);
4537
+ const output = await run(channel, "show configuration | display set | no-more", 180000);
4538
+ if (!/^set /m.test(output))
4539
+ throw new Error(`${device.name} returned no configuration`);
4540
+ return clean2(output);
4541
+ });
4542
+ }
4142
4543
  const client = await login2(device);
4143
4544
  try {
4144
- await checkVersion2(client, device);
4545
+ await checkVersion2((command) => exec(client, command), device);
4145
4546
  const output = await exec(client, "show configuration | display set | no-more", 180000);
4146
4547
  if (!/^set /m.test(output))
4147
4548
  throw new Error(`${device.name} returned no configuration`);
@@ -4164,6 +4565,34 @@ async function edit(channel, line, timeout = 60000) {
4164
4565
  const output = await readUntil(channel, (text) => EDIT_PROMPT.test(text.replace(/\r/g, "")), timeout, 300);
4165
4566
  return output.replace(/\r/g, "");
4166
4567
  }
4568
+ var JUNOS_CONSOLE = {
4569
+ login: /login: $/,
4570
+ password: /Password:\s*$/,
4571
+ prompt: /[>%] $/,
4572
+ logout: "exit",
4573
+ others: [[/# $/, `exit configuration-mode
4574
+ `]]
4575
+ };
4576
+ async function onConsole(device, work) {
4577
+ const { console: console2, user, password } = consoleOf(device);
4578
+ const opened = await openConsole(console2);
4579
+ const login = async () => {
4580
+ await consoleLogin(opened.channel, JUNOS_CONSOLE, user, password);
4581
+ const ready = openCli(opened.channel);
4582
+ opened.channel.write(`
4583
+ `);
4584
+ await ready;
4585
+ };
4586
+ try {
4587
+ await login();
4588
+ const result = await work(opened.channel, login);
4589
+ opened.channel.write(`${JUNOS_CONSOLE.logout}
4590
+ `);
4591
+ return result;
4592
+ } finally {
4593
+ opened.close();
4594
+ }
4595
+ }
4167
4596
  async function openCli(channel) {
4168
4597
  const banner = await readUntil(channel, (text) => PROMPT2.test(text.replace(/\r/g, "")), 20000, 500);
4169
4598
  if (/% $/.test(banner.replace(/\r/g, "")))
@@ -4267,9 +4696,23 @@ async function configure2(channel, plan, guard) {
4267
4696
  console.log(" logged in again, commit confirmed");
4268
4697
  }
4269
4698
  async function apply2(device, _network, plan, options) {
4699
+ if (options.console) {
4700
+ await onConsole(device, async (channel, relogin) => {
4701
+ await checkVersion2((command) => run(channel, command), device);
4702
+ await configure2(channel, plan, {
4703
+ minutes: options.rollback,
4704
+ prove: async () => {
4705
+ await run(channel, "exit configuration-mode");
4706
+ await relogin();
4707
+ await run(channel, "configure exclusive");
4708
+ }
4709
+ });
4710
+ });
4711
+ return;
4712
+ }
4270
4713
  const client = await login2(device);
4271
4714
  try {
4272
- await checkVersion2(client, device);
4715
+ await checkVersion2((command) => exec(client, command), device);
4273
4716
  const channel = await shell(client);
4274
4717
  await openCli(channel);
4275
4718
  await configure2(channel, plan, {
@@ -4527,7 +4970,7 @@ function fillPlan(set, prefixes) {
4527
4970
  async function fill2(device, _network, set, _family, prefixes) {
4528
4971
  const client = await login2(device);
4529
4972
  try {
4530
- await checkVersion2(client, device);
4973
+ await checkVersion2((command) => exec(client, command), device);
4531
4974
  const channel = await shell(client);
4532
4975
  await openCli(channel);
4533
4976
  await configure2(channel, fillPlan(set, prefixes));
@@ -4539,6 +4982,7 @@ async function fill2(device, _network, set, _family, prefixes) {
4539
4982
  }
4540
4983
  var junos = {
4541
4984
  extension: ".conf",
4985
+ console: true,
4542
4986
  unsupported: unsupported2,
4543
4987
  render: render2,
4544
4988
  read: read2,
@@ -4943,7 +5387,25 @@ function physicalPort(settings, vlanId, lagNumbers) {
4943
5387
  return lines;
4944
5388
  }
4945
5389
  var HOST_KEYS = ["ssh-rsa"];
4946
- async function open(device) {
5390
+ var RAISECOM_CONSOLE = {
5391
+ login: /login:\s*$/i,
5392
+ password: /password:\s*$/i,
5393
+ prompt: /[#>]\s*$/,
5394
+ logout: "exit",
5395
+ others: [[/\(config[^)\n]*\)#\s*$/, `end
5396
+ `]]
5397
+ };
5398
+ async function open(device, options = {}) {
5399
+ const session = options.console ? await consoleShell(device) : await sshShell(device);
5400
+ const version = await send(session.channel, "show version", 15000, 1500);
5401
+ if (!version.includes("ROS")) {
5402
+ session.close();
5403
+ throw new Error(`${device.name} did not answer as ROS`);
5404
+ }
5405
+ await send(session.channel, "terminal page-break disable", 15000, 1000);
5406
+ return session;
5407
+ }
5408
+ async function sshShell(device) {
4947
5409
  const user = device.users?.[device.connection.user];
4948
5410
  if (!user)
4949
5411
  throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
@@ -4960,16 +5422,45 @@ async function open(device) {
4960
5422
  client.end();
4961
5423
  };
4962
5424
  const banner = await readUntil(channel, (text) => /[#>]\s*$/.test(text.trimEnd()), 30000);
4963
- const version = await send(channel, "show version", 15000, 1500);
4964
- if (!/[#>]\s*$/.test(banner.trimEnd()) || !version.includes("ROS")) {
5425
+ if (!/[#>]\s*$/.test(banner.trimEnd())) {
4965
5426
  close();
4966
5427
  throw new Error(`${device.name} did not answer as ROS`);
4967
5428
  }
4968
- await send(channel, "terminal page-break disable", 15000, 1000);
4969
- return { channel, close };
5429
+ return {
5430
+ channel,
5431
+ close,
5432
+ fresh: async () => {
5433
+ close();
5434
+ return open(device);
5435
+ }
5436
+ };
5437
+ }
5438
+ async function consoleShell(device) {
5439
+ const { console: console2, user, password } = consoleOf(device);
5440
+ const opened = await openConsole(console2);
5441
+ try {
5442
+ await consoleLogin(opened.channel, RAISECOM_CONSOLE, user, password);
5443
+ } catch (error) {
5444
+ opened.close();
5445
+ throw error;
5446
+ }
5447
+ const session = {
5448
+ channel: opened.channel,
5449
+ close: () => {
5450
+ opened.channel.write(`${RAISECOM_CONSOLE.logout}
5451
+ `);
5452
+ opened.close();
5453
+ },
5454
+ fresh: async () => {
5455
+ await consoleLogin(opened.channel, RAISECOM_CONSOLE, user, password);
5456
+ await send(opened.channel, "terminal page-break disable", 15000, 1000);
5457
+ return session;
5458
+ }
5459
+ };
5460
+ return session;
4970
5461
  }
4971
- async function read3(device) {
4972
- const { channel, close } = await open(device);
5462
+ async function read3(device, _network = [], options = {}) {
5463
+ const { channel, close } = await open(device, options);
4973
5464
  try {
4974
5465
  const dump = await send(channel, "show running-config", 150000, 6000);
4975
5466
  const lines = dump.replace(/\r/g, "").split(`
@@ -5104,8 +5595,9 @@ function unsupported3(device) {
5104
5595
  var REFUSED3 = /^(%|Error|Incomplete|Unrecognized)|can not|cannot |is in use|Wrong parameter|invalid|unsuccessful/im;
5105
5596
  var CONFIRM = /^(portswitch|switchport trunk allowed vlan|switchport mode)/;
5106
5597
  var RELEASE = /^no (\S+ access-list \d+)$/;
5107
- async function apply3(device, _network, plan) {
5108
- const { channel, close } = await open(device);
5598
+ async function apply3(device, _network, plan, options) {
5599
+ const session = await open(device, options);
5600
+ const { channel } = session;
5109
5601
  const released = [];
5110
5602
  try {
5111
5603
  await send(channel, "config", 1e4);
@@ -5139,11 +5631,10 @@ async function apply3(device, _network, plan) {
5139
5631
  });
5140
5632
  if (released.length)
5141
5633
  console.log(` bound again: ${released.join(", ")}`);
5634
+ session.close();
5142
5635
  throw error;
5143
- } finally {
5144
- close();
5145
5636
  }
5146
- const proof = await open(device).catch(() => {
5637
+ const proof = await session.fresh().catch(() => {
5147
5638
  return;
5148
5639
  });
5149
5640
  if (!proof)
@@ -5213,7 +5704,7 @@ function plan3(desired, current) {
5213
5704
  }
5214
5705
  return { steps: planned, problems: [] };
5215
5706
  }
5216
- var raisecom = { extension: ".txt", unsupported: unsupported3, render: render3, read: read3, plan: plan3, apply: apply3 };
5707
+ var raisecom = { extension: ".txt", console: true, unsupported: unsupported3, render: render3, read: read3, plan: plan3, apply: apply3 };
5217
5708
 
5218
5709
  // src/adapters/devices/routeros/export.ts
5219
5710
  var unwrap = (text) => text.replace(/\r/g, "").replace(/\\\n\s*/g, "");
@@ -6851,8 +7342,56 @@ async function login3(device) {
6851
7342
  password: resolve(user.password)
6852
7343
  });
6853
7344
  }
6854
- async function checkVersion3(client, device) {
6855
- const version = (await exec(client, ":put [/system resource get version]")).trim();
7345
+ var ROUTEROS_CONSOLE = {
7346
+ login: /Login: $/,
7347
+ password: /Password: $/,
7348
+ prompt: /\] > $/,
7349
+ logout: "/quit",
7350
+ others: [
7351
+ [/-- \[Q quit\|D dump\|(?:C-z pause\|)?down\]$/, "D"],
7352
+ [/\[Y\/n\]:? ?$/, "n"]
7353
+ ],
7354
+ user: (user) => `${user}+cwt`
7355
+ };
7356
+ function sshSession(client, device) {
7357
+ return {
7358
+ run: (command, timeout) => exec(client, command, timeout),
7359
+ upload: (path, content) => upload(client, path, content),
7360
+ fresh: async () => {
7361
+ client.end();
7362
+ return sshSession(await login3(device), device);
7363
+ },
7364
+ end: () => client.end()
7365
+ };
7366
+ }
7367
+ async function consoleSession2(device) {
7368
+ const { console: console2, user, password } = consoleOf(device);
7369
+ const opened = await openConsole(console2);
7370
+ try {
7371
+ await consoleLogin(opened.channel, ROUTEROS_CONSOLE, user, password);
7372
+ } catch (error) {
7373
+ opened.close();
7374
+ throw error;
7375
+ }
7376
+ const shell = new ConsoleShell(opened.channel, ROUTEROS_CONSOLE);
7377
+ const session = {
7378
+ run: (command, timeout) => shell.run(command, timeout),
7379
+ fresh: async () => {
7380
+ await consoleLogin(opened.channel, ROUTEROS_CONSOLE, user, password);
7381
+ return session;
7382
+ },
7383
+ end: () => {
7384
+ shell.logout();
7385
+ opened.close();
7386
+ }
7387
+ };
7388
+ return session;
7389
+ }
7390
+ async function open2(device, options = {}) {
7391
+ return options.console ? consoleSession2(device) : sshSession(await login3(device), device);
7392
+ }
7393
+ async function checkVersion3(session, device) {
7394
+ const version = (await session.run(":put [/system resource get version]")).trim();
6856
7395
  const major = Number(version.split(".")[0]);
6857
7396
  if (major !== SUPPORTED_MAJOR2) {
6858
7397
  throw new Error(`${device.name} runs RouterOS ${version}; this adapter renders for version ${SUPPORTED_MAJOR2}`);
@@ -6867,13 +7406,13 @@ var LIST_CERTIFICATES = [
6867
7406
  ':put ("add fingerprint=" . [/certificate get $c fingerprint] . " name=" . [/certificate get $c name] . " private-key=" . $key)',
6868
7407
  "}"
6869
7408
  ].join(" ");
6870
- async function read4(device) {
6871
- const client = await login3(device);
7409
+ async function read4(device, _network = [], options = {}) {
7410
+ const session = await open2(device, options);
6872
7411
  try {
6873
- await checkVersion3(client, device);
6874
- const config = await exec(client, "/export", 300000);
6875
- const users = await exec(client, "/user export");
6876
- const certificates = (await exec(client, LIST_CERTIFICATES)).trim();
7412
+ await checkVersion3(session, device);
7413
+ const config = await session.run("/export", 300000);
7414
+ const users = await session.run("/user export");
7415
+ const certificates = (await session.run(LIST_CERTIFICATES)).trim();
6877
7416
  const listed = certificates.split(`
6878
7417
  `).every((line) => line.trim().startsWith("add fingerprint="));
6879
7418
  if (certificates && !listed) {
@@ -6888,7 +7427,7 @@ ${certificates}` : "";
6888
7427
  return withoutTimestamp(text) + `
6889
7428
  `;
6890
7429
  } finally {
6891
- client.end();
7430
+ session.end();
6892
7431
  }
6893
7432
  }
6894
7433
  function unsupported4(device) {
@@ -6945,36 +7484,38 @@ function unsupported4(device) {
6945
7484
  }
6946
7485
  var ROLLBACK_SCHEDULER = "apply-rollback";
6947
7486
  var escapeForString = (text) => text.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$");
6948
- async function refusal(client, command) {
6949
- const compiled = (await exec(client, `:put [:parse "${escapeForString(command)}"]`)).trim();
7487
+ async function refusal(session, command) {
7488
+ const compiled = (await session.run(`:put [:parse "${escapeForString(command)}"]`)).trim();
6950
7489
  const refused = !compiled.startsWith("(") || /syntax error|bad parameter|no such|expected/.test(compiled);
6951
7490
  return refused ? compiled.slice(0, 200) : undefined;
6952
7491
  }
6953
- async function installCertificate(client, device, name) {
7492
+ async function installCertificate(session, device, name) {
6954
7493
  const certificate = device.certificates?.[name];
6955
7494
  if (!certificate)
6956
7495
  throw new Error(`certificate ${name} is not declared`);
6957
- await upload(client, `${name}.crt`, certificate.certificate);
6958
- await exec(client, `/certificate import file-name=${name}.crt name=${name} passphrase=""`);
7496
+ if (!session.upload)
7497
+ throw new Error(`certificate ${name} is installed from a file, which only SSH can write; apply it without --console`);
7498
+ await session.upload(`${name}.crt`, certificate.certificate);
7499
+ await session.run(`/certificate import file-name=${name}.crt name=${name} passphrase=""`);
6959
7500
  if (certificate.private_key) {
6960
7501
  const value = resolve(certificate.private_key);
6961
7502
  const privateKey = value.includes("-----BEGIN") ? `${value}
6962
7503
  ` : Buffer.from(value, "base64").toString("utf8");
6963
- await upload(client, `${name}.key`, privateKey);
6964
- await exec(client, `/certificate import file-name=${name}.key name=${name} passphrase=""`);
7504
+ await session.upload(`${name}.key`, privateKey);
7505
+ await session.run(`/certificate import file-name=${name}.key name=${name} passphrase=""`);
6965
7506
  }
6966
- await exec(client, `/file remove [find name="${name}.crt" or name="${name}.key"]`);
7507
+ await session.run(`/file remove [find name="${name}.crt" or name="${name}.key"]`);
6967
7508
  }
6968
7509
  async function apply4(device, _network, changes, options) {
6969
- const client = await login3(device);
6970
- await checkVersion3(client, device);
7510
+ const session = await open2(device, options);
7511
+ await checkVersion3(session, device);
6971
7512
  const commands = changes.steps.flatMap((step) => step.send.map((command) => ({ title: step.title, command })));
6972
7513
  for (const { title, command } of commands) {
6973
7514
  if (command.startsWith(INSTALL_CERTIFICATE))
6974
7515
  continue;
6975
- const reason = await refusal(client, substitute(command));
7516
+ const reason = await refusal(session, substitute(command));
6976
7517
  if (reason) {
6977
- client.end();
7518
+ session.end();
6978
7519
  throw new Error(`${title}: the device will not parse ${command.slice(0, 200)}
6979
7520
  ${reason}`);
6980
7521
  }
@@ -6989,16 +7530,16 @@ async function apply4(device, _network, changes, options) {
6989
7530
  ].join(" ");
6990
7531
  try {
6991
7532
  console.log(` saving ${backup}, arming a ${options.rollback} minute rollback`);
6992
- await exec(client, `/system backup save name=${backup} dont-encrypt=yes`, 120000);
6993
- await exec(client, `/system scheduler remove [find name="${ROLLBACK_SCHEDULER}"]`);
6994
- await exec(client, armRollback);
7533
+ await session.run(`/system backup save name=${backup} dont-encrypt=yes`, 120000);
7534
+ await session.run(`/system scheduler remove [find name="${ROLLBACK_SCHEDULER}"]`);
7535
+ await session.run(armRollback);
6995
7536
  for (const { title, command } of commands) {
6996
7537
  if (command.startsWith(INSTALL_CERTIFICATE)) {
6997
7538
  const certificateName = command.slice(INSTALL_CERTIFICATE.length).trim();
6998
- await installCertificate(client, device, certificateName);
7539
+ await installCertificate(session, device, certificateName);
6999
7540
  continue;
7000
7541
  }
7001
- const answer = (await exec(client, substitute(command), 120000)).trim();
7542
+ const answer = (await session.run(substitute(command), 120000)).trim();
7002
7543
  if (answer)
7003
7544
  throw new Error(`${title}: ${command.slice(0, 160)}
7004
7545
  device said: ${answer.slice(0, 300)}`);
@@ -7006,20 +7547,19 @@ async function apply4(device, _network, changes, options) {
7006
7547
  } catch (error) {
7007
7548
  console.log(` FAILED: ${error.message}`);
7008
7549
  console.log(` restoring ${backup} now; the rollback stays armed in case this does not get through`);
7009
- await exec(client, `/system backup load name=${backup} password=""`, 30000).catch(() => {
7550
+ await session.run(`/system backup load name=${backup} password=""`, 30000).catch(() => {
7010
7551
  return;
7011
7552
  });
7012
- client.end();
7553
+ session.end();
7013
7554
  throw new Error("apply failed and the backup is being restored");
7014
7555
  }
7015
- client.end();
7016
- const proof = await login3(device).catch(() => {
7556
+ const proof = await session.fresh().catch(() => {
7017
7557
  return;
7018
7558
  });
7019
7559
  if (!proof)
7020
7560
  throw new Error(`could not log in again; the device restores ${backup} within ${options.rollback} minutes`);
7021
- await exec(proof, `/system scheduler remove [find name="${ROLLBACK_SCHEDULER}"]`);
7022
- await exec(proof, `/file remove [find name="${backup}.backup"]`);
7561
+ await proof.run(`/system scheduler remove [find name="${ROLLBACK_SCHEDULER}"]`);
7562
+ await proof.run(`/file remove [find name="${backup}.backup"]`);
7023
7563
  proof.end();
7024
7564
  console.log(" logged in again, rollback disarmed");
7025
7565
  }
@@ -7043,6 +7583,7 @@ async function fill3(device, _network, set, family, prefixes) {
7043
7583
  }
7044
7584
  var routeros = {
7045
7585
  extension: ".rsc",
7586
+ console: true,
7046
7587
  unsupported: unsupported4,
7047
7588
  render: render4,
7048
7589
  read: read4,
@@ -7515,25 +8056,60 @@ async function openShell(client) {
7515
8056
  await send(channel, "screen-length 0 temporary", 1e4, 1500);
7516
8057
  return channel;
7517
8058
  }
7518
- async function read5(device) {
8059
+ var VRP_CONSOLE = {
8060
+ login: /Username:\s*$/i,
8061
+ password: /Password:\s*$/i,
8062
+ prompt: /<[^>\n]+>\s*$/,
8063
+ logout: "quit",
8064
+ others: [
8065
+ [/Change now\? ?\[Y\/N\]:?\s*$/i, `N
8066
+ `],
8067
+ [/\[~?\*?[^\]\n]+\]\s*$/, `return
8068
+ `]
8069
+ ]
8070
+ };
8071
+ async function onConsole2(device, work) {
8072
+ const { console: console2, user, password } = consoleOf(device);
8073
+ const opened = await openConsole(console2);
8074
+ const login = async () => {
8075
+ await consoleLogin(opened.channel, VRP_CONSOLE, user, password);
8076
+ await send(opened.channel, "screen-length 0 temporary", 1e4, 1500);
8077
+ };
8078
+ try {
8079
+ await login();
8080
+ const result = await work(opened.channel, login);
8081
+ opened.channel.write(`${VRP_CONSOLE.logout}
8082
+ `);
8083
+ return result;
8084
+ } finally {
8085
+ opened.close();
8086
+ }
8087
+ }
8088
+ async function read5(device, _network = [], options = {}) {
8089
+ if (options.console) {
8090
+ return onConsole2(device, async (channel) => configurationOf(device, await send(channel, "display current-configuration", 120000, 3000)));
8091
+ }
7519
8092
  const client = await login4(device);
7520
8093
  try {
7521
8094
  const channel = await openShell(client);
7522
8095
  const dump = await send(channel, "display current-configuration", 120000, 3000);
7523
8096
  channel.end();
7524
- const lines = dump.replace(/\r/g, "").split(`
7525
- `);
7526
- const start = lines.findIndex((line) => line.startsWith("!Software Version"));
7527
- if (start < 0)
7528
- throw new Error(`${device.name} returned no configuration`);
7529
- const body = lines.slice(start).filter((line) => !/^<.*>$/.test(line.trim()) && !line.startsWith("!Last configuration"));
7530
- return scrub3(body.join(`
7531
- `).trim() + `
7532
- `);
8097
+ return configurationOf(device, dump);
7533
8098
  } finally {
7534
8099
  client.end();
7535
8100
  }
7536
8101
  }
8102
+ function configurationOf(device, dump) {
8103
+ const lines = dump.replace(/\r/g, "").split(`
8104
+ `);
8105
+ const start = lines.findIndex((line) => line.startsWith("!Software Version"));
8106
+ if (start < 0)
8107
+ throw new Error(`${device.name} returned no configuration`);
8108
+ const body = lines.slice(start).filter((line) => !/^<.*>$/.test(line.trim()) && !line.startsWith("!Last configuration"));
8109
+ return scrub3(body.join(`
8110
+ `).trim() + `
8111
+ `);
8112
+ }
7537
8113
  function unsupported5(device) {
7538
8114
  const found = [];
7539
8115
  for (const [name, port] of Object.entries(device.ports ?? {})) {
@@ -7630,9 +8206,7 @@ async function sendLine(channel, line) {
7630
8206
  answer += await send(channel, "Y", 20000, 800);
7631
8207
  return answer;
7632
8208
  }
7633
- async function apply5(device, _network, plan) {
7634
- const client = await login4(device);
7635
- const channel = await openShell(client);
8209
+ async function commitPlan(channel, plan) {
7636
8210
  try {
7637
8211
  await send(channel, "system-view", 1e4, 1500);
7638
8212
  for (const step of plan.steps) {
@@ -7651,24 +8225,42 @@ async function apply5(device, _network, plan) {
7651
8225
  await send(channel, "clear configuration candidate", 1e4).catch(() => {
7652
8226
  return;
7653
8227
  });
7654
- channel.end();
7655
- client.end();
7656
8228
  throw error;
7657
8229
  }
7658
- const proof = await login4(device).catch(() => {
8230
+ }
8231
+ async function save(channel) {
8232
+ const saving = await send(channel, "save", 20000, 2000);
8233
+ if (/\[Y\/N\]/i.test(saving))
8234
+ await send(channel, "Y", 60000, 5000);
8235
+ }
8236
+ var NOT_SAVED = "Not saved: a reboot restores the previous configuration.";
8237
+ async function apply5(device, _network, plan, options) {
8238
+ if (options.console) {
8239
+ await onConsole2(device, async (channel, relogin) => {
8240
+ await commitPlan(channel, plan);
8241
+ await relogin().catch((error) => {
8242
+ throw new Error(`committed, but a fresh login on the console failed: ${error.message}. ${NOT_SAVED}`);
8243
+ });
8244
+ await save(channel);
8245
+ });
8246
+ console.log(" committed, login proved, saved");
7659
8247
  return;
7660
- });
7661
- if (!proof) {
8248
+ }
8249
+ const client = await login4(device);
8250
+ const channel = await openShell(client);
8251
+ try {
8252
+ await commitPlan(channel, plan);
8253
+ const proof = await login4(device).catch(() => {
8254
+ return;
8255
+ });
8256
+ if (!proof)
8257
+ throw new Error(`committed, but a fresh login failed. ${NOT_SAVED} Recover over the open session or the console.`);
8258
+ proof.end();
8259
+ await save(channel);
8260
+ } finally {
7662
8261
  channel.end();
7663
8262
  client.end();
7664
- throw new Error("committed, but a fresh login failed. Not saved: a reboot restores the previous configuration. Recover over the open session or the console.");
7665
8263
  }
7666
- proof.end();
7667
- const saving = await send(channel, "save", 20000, 2000);
7668
- if (/\[Y\/N\]/i.test(saving))
7669
- await send(channel, "Y", 60000, 5000);
7670
- channel.end();
7671
- client.end();
7672
8264
  console.log(" committed, login proved, saved");
7673
8265
  }
7674
8266
  function plan5(desired, current) {
@@ -7693,6 +8285,7 @@ function plan5(desired, current) {
7693
8285
  }
7694
8286
  var vrp = {
7695
8287
  extension: ".cfg",
8288
+ console: true,
7696
8289
  unsupported: unsupported5,
7697
8290
  render: render5,
7698
8291
  read: read5,
@@ -8661,6 +9254,7 @@ var HELP = `usage: circuit <command> [device...] [options]
8661
9254
  apply <device> read the device, plan against what it runs, and send with --confirm
8662
9255
  --secrets also send every secret, to rotate one
8663
9256
  --rollback=N minutes before a device that can restore itself does, default 10
9257
+ --console through the device's serial console on its jump host, instead of SSH; also for snapshot
8664
9258
  refresh <device> fetch prefix sets that come from a registry onto the device, with --confirm
8665
9259
  wireguard <device> <interface> <peer>
8666
9260
  a client config for one peer, its private key left for the peer to fill
@@ -8721,7 +9315,8 @@ var names = rest.filter((arg) => !arg.startsWith("--"));
8721
9315
  var confirmed = flags.includes("--confirm");
8722
9316
  var options = {
8723
9317
  secrets: flags.includes("--secrets"),
8724
- rollback: Number(flags.find((flag) => flag.startsWith("--rollback="))?.split("=")[1] ?? 10)
9318
+ rollback: Number(flags.find((flag) => flag.startsWith("--rollback="))?.split("=")[1] ?? 10),
9319
+ ...flags.includes("--console") ? { console: true } : {}
8725
9320
  };
8726
9321
  function fail(message) {
8727
9322
  console.error(message);
@@ -8738,7 +9333,16 @@ function one() {
8738
9333
  const [device] = selected(false);
8739
9334
  return device ?? fail("name a device");
8740
9335
  }
8741
- var adapterOf = (device) => adapters[device.platform] ?? fail(`no adapter for ${device.platform}`);
9336
+ function adapterOf(device) {
9337
+ const adapter = adapters[device.platform] ?? fail(`no adapter for ${device.platform}`);
9338
+ if (!options.console)
9339
+ return adapter;
9340
+ if (!adapter.console)
9341
+ fail(`${device.name}: ${device.platform} cannot be reached through a console yet`);
9342
+ if (!device.connection.console)
9343
+ fail(`${device.name}: --console needs connection.console`);
9344
+ return adapter;
9345
+ }
8742
9346
  var generated = join4(root2, ".circuit");
8743
9347
  var stateDirectory = join4(generated, "state");
8744
9348
  var statePath = (device) => join4(stateDirectory, device.name + adapterOf(device).extension);
@@ -8805,7 +9409,7 @@ function diffCommand() {
8805
9409
  }
8806
9410
  async function snapshotCommand() {
8807
9411
  for (const device of selected(true)) {
8808
- const text = await adapterOf(device).read(device, network3.devices);
9412
+ const text = await adapterOf(device).read(device, network3.devices, options);
8809
9413
  writeState(device, text);
8810
9414
  console.log(`${device.name}: ${text.split(`
8811
9415
  `).length} lines`);
@@ -8824,7 +9428,7 @@ async function applyCommand() {
8824
9428
  const device = one();
8825
9429
  guard([device]);
8826
9430
  const adapter = adapterOf(device);
8827
- const live = await adapter.read(device, network3.devices);
9431
+ const live = await adapter.read(device, network3.devices, options);
8828
9432
  writeState(device, live);
8829
9433
  const plan = adapter.plan(adapter.render(device), live, options);
8830
9434
  if (!show(device, plan))
@@ -8838,7 +9442,7 @@ Nothing sent. Re-run with --confirm.`);
8838
9442
  return;
8839
9443
  }
8840
9444
  await adapter.apply(device, network3.devices, plan, options);
8841
- writeState(device, await adapter.read(device, network3.devices));
9445
+ writeState(device, await adapter.read(device, network3.devices, options));
8842
9446
  console.log("snapshot updated");
8843
9447
  }
8844
9448
  async function refreshCommand() {