@machinen/cli 0.3.3 → 0.4.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.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  symlinkSync,
14
14
  unlinkSync
15
15
  } from "fs";
16
- import { homedir as homedir2, tmpdir } from "os";
16
+ import { arch as osArch, homedir as homedir2, tmpdir } from "os";
17
17
  import { dirname as dirname2, join as join2, resolve } from "path";
18
18
  import { PassThrough, Transform } from "stream";
19
19
  import { pipeline } from "stream/promises";
@@ -33,7 +33,7 @@ import debugLib2 from "debug";
33
33
  // package.json
34
34
  var package_default = {
35
35
  name: "@machinen/cli",
36
- version: "0.3.3",
36
+ version: "0.4.0",
37
37
  license: "FSL-1.1-MIT",
38
38
  repository: {
39
39
  type: "git",
@@ -64,7 +64,8 @@ var package_default = {
64
64
  },
65
65
  optionalDependencies: {
66
66
  "@machinen/native-arm64-darwin": "workspace:*",
67
- "@machinen/native-arm64-linux": "workspace:*"
67
+ "@machinen/native-arm64-linux": "workspace:*",
68
+ "@machinen/native-x64-linux": "workspace:*"
68
69
  }
69
70
  };
70
71
 
@@ -95,7 +96,7 @@ var COMMANDS = [
95
96
  name: "--mount-live",
96
97
  type: "string",
97
98
  repeatable: true,
98
- description: "Live-share host dir. Spec: <host>:<guest>[:rw|ro][:fuse|virtiofs]."
99
+ description: "Live-share host dir over virtio-fs. Spec: <host>:<guest>[:rw|ro]."
99
100
  },
100
101
  {
101
102
  name: "--env",
@@ -122,6 +123,11 @@ var COMMANDS = [
122
123
  description: "Detach the VMM from the CLI on first-guest-byte readiness."
123
124
  },
124
125
  { name: "--memory", type: "integer", description: "Guest RAM ceiling in MiB (debug knob)." },
126
+ {
127
+ name: "--nested",
128
+ type: "boolean",
129
+ description: "Expose arm64 EL2 / /dev/kvm to the guest when the host supports it."
130
+ },
125
131
  {
126
132
  name: "--json",
127
133
  type: "boolean",
@@ -144,7 +150,7 @@ var COMMANDS = [
144
150
  {
145
151
  name: "--lazy",
146
152
  type: "boolean",
147
- description: "Opt into lazy-pages restore (#266) \u2014 vsock-FUSE mount the bundle and fault pages on demand. Default is eager."
153
+ description: "Opt into CRIU lazy-pages restore (#266) \u2014 virtio-fs mount the bundle and fault pages on demand. Default is eager."
148
154
  },
149
155
  {
150
156
  name: "-p",
@@ -237,7 +243,7 @@ var COMMANDS = [
237
243
  {
238
244
  name: "--lazy",
239
245
  type: "boolean",
240
- description: "Opt into lazy-pages restore (#266); ignored when --detach is set since the FUSE server can't survive supervisor exit."
246
+ description: "Opt into CRIU lazy-pages restore (#266); the CLI currently ignores this when --detach is set."
241
247
  },
242
248
  {
243
249
  name: "-p",
@@ -486,120 +492,175 @@ import { ParseError as ParseError2 } from "@machinen/runtime";
486
492
 
487
493
  // src/parse-run-args.ts
488
494
  import { ParseError } from "@machinen/runtime";
495
+ var RUN_VALUE_FLAGS = /* @__PURE__ */ new Map([
496
+ ["--mount", "mount"],
497
+ ["--mount-live", "liveMount"],
498
+ ["--env", "env"],
499
+ ["-p", "portForward"],
500
+ ["--publish", "portForward"],
501
+ ["--snapshot", "snapshot"],
502
+ ["--cwd", "guestCwd"],
503
+ ["--name", "name"],
504
+ ["--memory", "memory"]
505
+ ]);
506
+ var RUN_BARE_FLAGS = /* @__PURE__ */ new Map([
507
+ ["--nested", "nested"],
508
+ ["--detached", "detach"],
509
+ ["--detach", "detach"],
510
+ ["--json", "json"]
511
+ ]);
512
+ var RUN_FLAG_HANDLERS = {
513
+ mount: handleRunMount,
514
+ liveMount: handleRunLiveMount,
515
+ env: handleRunEnv,
516
+ portForward: handleRunPortForward,
517
+ snapshot: handleRunSnapshot,
518
+ nested: handleRunNested,
519
+ nestedValue: handleRunNestedValue,
520
+ guestCwd: handleRunGuestCwd,
521
+ name: handleRunName,
522
+ detach: handleRunDetach,
523
+ json: handleRunJson,
524
+ memory: handleRunMemory
525
+ };
489
526
  function parseRunArgs(argv) {
490
- const idx = argv.indexOf("--");
491
- const pre = idx === -1 ? argv : argv.slice(0, idx);
492
- const double_dash_args = idx === -1 ? [] : argv.slice(idx + 1);
493
- const positional = [];
494
- let mount;
495
- const liveMounts = [];
496
- const env = {};
497
- const portForward = [];
498
- const seenHostPorts = /* @__PURE__ */ new Set();
499
- let snapshot;
500
- let name;
501
- let guestCwd;
502
- let detached = false;
503
- let memory;
504
- let json = false;
527
+ const { pre, double_dash_args } = splitCommandArgs(argv);
528
+ const state = newRunParseState();
505
529
  for (let i = 0; i < pre.length; i++) {
506
- const a = pre[i];
507
- if (a === "--mount" || a.startsWith("--mount=")) {
508
- if (mount) {
509
- throw new ParseError(
510
- "PARSE_FLAG_DUPLICATE",
511
- "--mount may be given at most once per invocation"
512
- );
513
- }
514
- const r = consumeMount(a, pre, i);
515
- mount = r.value;
516
- i = r.next;
517
- } else if (a === "--mount-live" || a.startsWith("--mount-live=")) {
518
- const r = consumeLiveMount(a, pre, i);
519
- liveMounts.push(r.value);
520
- i = r.next;
521
- } else if (a === "--env" || a.startsWith("--env=")) {
522
- const r = consumeEnv(a, pre, i);
523
- env[r.key] = r.value;
524
- i = r.next;
525
- } else if (a === "-p" || a === "--publish" || a.startsWith("-p=") || a.startsWith("--publish=")) {
526
- i = consumePortForward(a, pre, i, seenHostPorts, portForward);
527
- } else if (a === "--snapshot" || a.startsWith("--snapshot=")) {
528
- if (snapshot) {
529
- throw new ParseError(
530
- "PARSE_FLAG_DUPLICATE",
531
- "--snapshot may be given at most once per invocation"
532
- );
533
- }
534
- const { spec, next } = takeValue(a, pre, i, "a path value");
535
- snapshot = spec;
536
- i = next;
537
- } else if (a === "--cwd" || a.startsWith("--cwd=")) {
538
- if (guestCwd !== void 0) {
539
- throw new ParseError(
540
- "PARSE_FLAG_DUPLICATE",
541
- "--cwd may be given at most once per invocation"
542
- );
543
- }
544
- const r = consumeGuestCwd(a, pre, i);
545
- guestCwd = r.value;
546
- i = r.next;
547
- } else if (a === "--name" || a.startsWith("--name=")) {
548
- if (name) {
549
- throw new ParseError(
550
- "PARSE_FLAG_DUPLICATE",
551
- "--name may be given at most once per invocation"
552
- );
553
- }
554
- const { spec, next } = takeValue(a, pre, i, "a value");
555
- name = spec;
556
- i = next;
557
- } else if (a === "--detached" || a === "--detach") {
558
- if (detached) {
559
- throw new ParseError(
560
- "PARSE_FLAG_DUPLICATE",
561
- "--detached may be given at most once per invocation"
562
- );
563
- }
564
- detached = true;
565
- if (a === "--detached" && !process.env.MACHINEN_QUIET_DEPRECATIONS && !process.env.VITEST) {
566
- process.stderr.write(
567
- "machinen: --detached is deprecated; use --detach (same behaviour).\n"
568
- );
569
- }
570
- } else if (a === "--json") {
571
- json = true;
572
- } else if (a === "--memory" || a.startsWith("--memory=")) {
573
- if (memory !== void 0) {
574
- throw new ParseError(
575
- "PARSE_FLAG_DUPLICATE",
576
- "--memory may be given at most once per invocation"
577
- );
578
- }
579
- const r = consumeMemory(a, pre, i);
580
- memory = r.value;
581
- i = r.next;
582
- } else if (a.startsWith("-")) {
583
- throw new ParseError("PARSE_FLAG_UNKNOWN", `unknown flag: ${a}`);
584
- } else {
585
- positional.push(a);
530
+ const arg = pre[i];
531
+ const flag = runFlagFor(arg);
532
+ if (flag) {
533
+ i = RUN_FLAG_HANDLERS[flag](state, arg, pre, i);
534
+ continue;
586
535
  }
536
+ if (arg.startsWith("-")) {
537
+ throw new ParseError("PARSE_FLAG_UNKNOWN", `unknown flag: ${arg}`);
538
+ }
539
+ state.positional.push(arg);
540
+ }
541
+ return finishRunArgs(state, double_dash_args);
542
+ }
543
+ function splitCommandArgs(argv) {
544
+ const idx = argv.indexOf("--");
545
+ if (idx === -1) {
546
+ return { pre: argv, double_dash_args: [] };
547
+ }
548
+ return { pre: argv.slice(0, idx), double_dash_args: argv.slice(idx + 1) };
549
+ }
550
+ function newRunParseState() {
551
+ return {
552
+ positional: [],
553
+ liveMounts: [],
554
+ env: {},
555
+ portForward: [],
556
+ seenHostPorts: /* @__PURE__ */ new Set(),
557
+ nested: false,
558
+ detached: false,
559
+ json: false
560
+ };
561
+ }
562
+ function runFlagFor(arg) {
563
+ const eq = arg.indexOf("=");
564
+ if (eq !== -1) {
565
+ const head = arg.slice(0, eq);
566
+ if (head === "--nested") {
567
+ return "nestedValue";
568
+ }
569
+ return RUN_VALUE_FLAGS.get(head);
587
570
  }
571
+ return RUN_BARE_FLAGS.get(arg) ?? RUN_VALUE_FLAGS.get(arg);
572
+ }
573
+ function finishRunArgs(state, double_dash_args) {
588
574
  return {
589
- positional,
575
+ positional: state.positional,
590
576
  double_dash_args,
591
- mount,
592
- liveMounts: liveMounts.length > 0 ? liveMounts : void 0,
593
- env: Object.keys(env).length > 0 ? env : void 0,
594
- portForward: portForward.length > 0 ? portForward : void 0,
595
- snapshot,
596
- name,
597
- guestCwd,
598
- detached: detached || void 0,
599
- memory,
600
- json: json || void 0
577
+ mount: state.mount,
578
+ liveMounts: state.liveMounts.length > 0 ? state.liveMounts : void 0,
579
+ env: Object.keys(state.env).length > 0 ? state.env : void 0,
580
+ portForward: state.portForward.length > 0 ? state.portForward : void 0,
581
+ snapshot: state.snapshot,
582
+ nested: state.nested || void 0,
583
+ name: state.name,
584
+ guestCwd: state.guestCwd,
585
+ detached: state.detached || void 0,
586
+ memory: state.memory,
587
+ json: state.json || void 0
601
588
  };
602
589
  }
590
+ function handleRunMount(state, flag, args, index) {
591
+ assertRunFlagUnused(state.mount !== void 0, "--mount");
592
+ const result = consumeMount(flag, args, index);
593
+ state.mount = result.value;
594
+ return result.next;
595
+ }
596
+ function handleRunLiveMount(state, flag, args, index) {
597
+ const result = consumeLiveMount(flag, args, index);
598
+ state.liveMounts.push(result.value);
599
+ return result.next;
600
+ }
601
+ function handleRunEnv(state, flag, args, index) {
602
+ const result = consumeEnv(flag, args, index);
603
+ state.env[result.key] = result.value;
604
+ return result.next;
605
+ }
606
+ function handleRunPortForward(state, flag, args, index) {
607
+ return consumePortForward(flag, args, index, state.seenHostPorts, state.portForward);
608
+ }
609
+ function handleRunSnapshot(state, flag, args, index) {
610
+ assertRunFlagUnused(state.snapshot !== void 0, "--snapshot");
611
+ const { spec, next } = takeValue(flag, args, index, "a path value");
612
+ state.snapshot = spec;
613
+ return next;
614
+ }
615
+ function handleRunNested(state, _flag, _args, index) {
616
+ assertRunFlagUnused(state.nested, "--nested");
617
+ state.nested = true;
618
+ return index;
619
+ }
620
+ function handleRunNestedValue() {
621
+ throw new ParseError("PARSE_FLAG_MALFORMED", "--nested does not take a value");
622
+ }
623
+ function handleRunGuestCwd(state, flag, args, index) {
624
+ assertRunFlagUnused(state.guestCwd !== void 0, "--cwd");
625
+ const result = consumeGuestCwd(flag, args, index);
626
+ state.guestCwd = result.value;
627
+ return result.next;
628
+ }
629
+ function handleRunName(state, flag, args, index) {
630
+ assertRunFlagUnused(state.name !== void 0, "--name");
631
+ const { spec, next } = takeValue(flag, args, index, "a value");
632
+ state.name = spec;
633
+ return next;
634
+ }
635
+ function handleRunDetach(state, flag, _args, index) {
636
+ assertRunFlagUnused(state.detached, "--detached");
637
+ state.detached = true;
638
+ warnDeprecatedDetached(flag);
639
+ return index;
640
+ }
641
+ function handleRunJson(state, _flag, _args, index) {
642
+ state.json = true;
643
+ return index;
644
+ }
645
+ function handleRunMemory(state, flag, args, index) {
646
+ assertRunFlagUnused(state.memory !== void 0, "--memory");
647
+ const result = consumeMemory(flag, args, index);
648
+ state.memory = result.value;
649
+ return result.next;
650
+ }
651
+ function assertRunFlagUnused(used, flag) {
652
+ if (used) {
653
+ throw new ParseError(
654
+ "PARSE_FLAG_DUPLICATE",
655
+ `${flag} may be given at most once per invocation`
656
+ );
657
+ }
658
+ }
659
+ function warnDeprecatedDetached(flag) {
660
+ if (flag === "--detached" && !process.env.MACHINEN_QUIET_DEPRECATIONS && !process.env.VITEST) {
661
+ process.stderr.write("machinen: --detached is deprecated; use --detach (same behaviour).\n");
662
+ }
663
+ }
603
664
  function parsePort(raw, label) {
604
665
  if (!/^[0-9]+$/.test(raw)) {
605
666
  throw new ParseError("PARSE_PORT_INVALID", `-p: ${label} must be numeric (got '${raw}')`);
@@ -715,167 +776,255 @@ function consumeGuestCwd(flag, args, i) {
715
776
  }
716
777
 
717
778
  // src/parse-fork-args.ts
779
+ var FORK_VALUE_FLAGS = /* @__PURE__ */ new Map([
780
+ ["--new-name", "newName"],
781
+ ["--out-dir", "outDir"],
782
+ ["-p", "portForward"],
783
+ ["--publish", "portForward"],
784
+ ["--mount", "mount"],
785
+ ["--mount-live", "liveMount"],
786
+ ["--env", "env"],
787
+ ["--cwd", "guestCwd"],
788
+ ["--memory", "memory"]
789
+ ]);
790
+ var FORK_BARE_FLAGS = /* @__PURE__ */ new Map([
791
+ ["--tcp-keep", "tcpKeep"],
792
+ ["--detach", "detach"],
793
+ ["--lazy", "lazy"]
794
+ ]);
795
+ var FORK_FLAG_HANDLERS = {
796
+ newName: handleForkNewName,
797
+ outDir: handleForkOutDir,
798
+ tcpKeep: handleForkTcpKeep,
799
+ detach: handleForkDetach,
800
+ lazy: handleForkLazy,
801
+ portForward: handleForkPortForward,
802
+ mount: handleForkMount,
803
+ liveMount: handleForkLiveMount,
804
+ env: handleForkEnv,
805
+ guestCwd: handleForkGuestCwd,
806
+ memory: handleForkMemory
807
+ };
718
808
  function parseForkArgs(argv) {
719
- let newName;
720
- let outDir;
721
- let tcpKeep = false;
722
- let detach = false;
723
- let lazy = false;
724
- const portForward = [];
725
- const seenHostPorts = /* @__PURE__ */ new Set();
726
- let mount;
727
- const liveMounts = [];
728
- const env = {};
729
- let guestCwd;
730
- let memory;
731
- const rest = [];
809
+ const state = newForkParseState();
732
810
  for (let i = 0; i < argv.length; i++) {
733
- const a = argv[i];
734
- if (a === "--new-name" || a.startsWith("--new-name=")) {
735
- if (newName !== void 0) {
736
- throw new ParseError2(
737
- "PARSE_FLAG_DUPLICATE",
738
- "--new-name may be given at most once per invocation"
739
- );
740
- }
741
- const { spec, next } = takeValue(a, argv, i, "a value");
742
- newName = spec;
743
- i = next;
744
- } else if (a === "--out-dir" || a.startsWith("--out-dir=")) {
745
- if (outDir !== void 0) {
746
- throw new ParseError2(
747
- "PARSE_FLAG_DUPLICATE",
748
- "--out-dir may be given at most once per invocation"
749
- );
750
- }
751
- const { spec, next } = takeValue(a, argv, i, "a directory path");
752
- outDir = spec;
753
- i = next;
754
- } else if (a === "--tcp-keep") {
755
- tcpKeep = true;
756
- } else if (a === "--detach") {
757
- detach = true;
758
- } else if (a === "--lazy") {
759
- lazy = true;
760
- } else if (a === "-p" || a === "--publish" || a.startsWith("-p=") || a.startsWith("--publish=")) {
761
- i = consumePortForward(a, argv, i, seenHostPorts, portForward);
762
- } else if (a === "--mount" || a.startsWith("--mount=")) {
763
- if (mount) {
764
- throw new ParseError2(
765
- "PARSE_FLAG_DUPLICATE",
766
- "--mount may be given at most once per invocation"
767
- );
768
- }
769
- const r = consumeMount(a, argv, i);
770
- mount = r.value;
771
- i = r.next;
772
- } else if (a === "--mount-live" || a.startsWith("--mount-live=")) {
773
- const r = consumeLiveMount(a, argv, i);
774
- liveMounts.push(r.value);
775
- i = r.next;
776
- } else if (a === "--env" || a.startsWith("--env=")) {
777
- const r = consumeEnv(a, argv, i);
778
- env[r.key] = r.value;
779
- i = r.next;
780
- } else if (a === "--cwd" || a.startsWith("--cwd=")) {
781
- if (guestCwd !== void 0) {
782
- throw new ParseError2(
783
- "PARSE_FLAG_DUPLICATE",
784
- "--cwd may be given at most once per invocation"
785
- );
786
- }
787
- const r = consumeGuestCwd(a, argv, i);
788
- guestCwd = r.value;
789
- i = r.next;
790
- } else if (a === "--memory" || a.startsWith("--memory=")) {
791
- if (memory !== void 0) {
792
- throw new ParseError2(
793
- "PARSE_FLAG_DUPLICATE",
794
- "--memory may be given at most once per invocation"
795
- );
796
- }
797
- const r = consumeMemory(a, argv, i);
798
- memory = r.value;
799
- i = r.next;
800
- } else {
801
- rest.push(a);
811
+ const arg = argv[i];
812
+ const flag = forkFlagFor(arg);
813
+ if (flag) {
814
+ i = FORK_FLAG_HANDLERS[flag](state, arg, argv, i);
815
+ continue;
802
816
  }
817
+ state.rest.push(arg);
818
+ }
819
+ return finishForkArgs(state);
820
+ }
821
+ function newForkParseState() {
822
+ return {
823
+ tcpKeep: false,
824
+ detach: false,
825
+ lazy: false,
826
+ portForward: [],
827
+ seenHostPorts: /* @__PURE__ */ new Set(),
828
+ liveMounts: [],
829
+ env: {},
830
+ rest: []
831
+ };
832
+ }
833
+ function forkFlagFor(arg) {
834
+ const eq = arg.indexOf("=");
835
+ if (eq !== -1) {
836
+ return FORK_VALUE_FLAGS.get(arg.slice(0, eq));
803
837
  }
838
+ return FORK_BARE_FLAGS.get(arg) ?? FORK_VALUE_FLAGS.get(arg);
839
+ }
840
+ function finishForkArgs(state) {
804
841
  return {
805
- newName,
806
- outDir,
807
- tcpKeep,
808
- detach,
809
- lazy: lazy && !detach,
810
- portForward,
811
- mount,
812
- liveMounts: liveMounts.length > 0 ? liveMounts : void 0,
813
- env: Object.keys(env).length > 0 ? env : void 0,
814
- guestCwd,
815
- memory,
816
- rest
842
+ newName: state.newName,
843
+ outDir: state.outDir,
844
+ tcpKeep: state.tcpKeep,
845
+ detach: state.detach,
846
+ lazy: state.lazy && !state.detach,
847
+ portForward: state.portForward,
848
+ mount: state.mount,
849
+ liveMounts: state.liveMounts.length > 0 ? state.liveMounts : void 0,
850
+ env: Object.keys(state.env).length > 0 ? state.env : void 0,
851
+ guestCwd: state.guestCwd,
852
+ memory: state.memory,
853
+ rest: state.rest
817
854
  };
818
855
  }
856
+ function handleForkNewName(state, flag, args, index) {
857
+ assertForkFlagUnused(state.newName !== void 0, "--new-name");
858
+ const { spec, next } = takeValue(flag, args, index, "a value");
859
+ state.newName = spec;
860
+ return next;
861
+ }
862
+ function handleForkOutDir(state, flag, args, index) {
863
+ assertForkFlagUnused(state.outDir !== void 0, "--out-dir");
864
+ const { spec, next } = takeValue(flag, args, index, "a directory path");
865
+ state.outDir = spec;
866
+ return next;
867
+ }
868
+ function handleForkTcpKeep(state, _flag, _args, index) {
869
+ state.tcpKeep = true;
870
+ return index;
871
+ }
872
+ function handleForkDetach(state, _flag, _args, index) {
873
+ state.detach = true;
874
+ return index;
875
+ }
876
+ function handleForkLazy(state, _flag, _args, index) {
877
+ state.lazy = true;
878
+ return index;
879
+ }
880
+ function handleForkPortForward(state, flag, args, index) {
881
+ return consumePortForward(flag, args, index, state.seenHostPorts, state.portForward);
882
+ }
883
+ function handleForkMount(state, flag, args, index) {
884
+ assertForkFlagUnused(state.mount !== void 0, "--mount");
885
+ const result = consumeMount(flag, args, index);
886
+ state.mount = result.value;
887
+ return result.next;
888
+ }
889
+ function handleForkLiveMount(state, flag, args, index) {
890
+ const result = consumeLiveMount(flag, args, index);
891
+ state.liveMounts.push(result.value);
892
+ return result.next;
893
+ }
894
+ function handleForkEnv(state, flag, args, index) {
895
+ const result = consumeEnv(flag, args, index);
896
+ state.env[result.key] = result.value;
897
+ return result.next;
898
+ }
899
+ function handleForkGuestCwd(state, flag, args, index) {
900
+ assertForkFlagUnused(state.guestCwd !== void 0, "--cwd");
901
+ const result = consumeGuestCwd(flag, args, index);
902
+ state.guestCwd = result.value;
903
+ return result.next;
904
+ }
905
+ function handleForkMemory(state, flag, args, index) {
906
+ assertForkFlagUnused(state.memory !== void 0, "--memory");
907
+ const result = consumeMemory(flag, args, index);
908
+ state.memory = result.value;
909
+ return result.next;
910
+ }
911
+ function assertForkFlagUnused(used, flag) {
912
+ if (used) {
913
+ throw new ParseError2(
914
+ "PARSE_FLAG_DUPLICATE",
915
+ `${flag} may be given at most once per invocation`
916
+ );
917
+ }
918
+ }
819
919
 
820
920
  // src/parse-restore-args.ts
821
921
  import { ParseError as ParseError3 } from "@machinen/runtime";
922
+ var RESTORE_VALUE_FLAGS = /* @__PURE__ */ new Map([
923
+ ["--name", "name"],
924
+ ["--image", "image"],
925
+ ["--mount-live", "liveMount"],
926
+ ["-p", "portForward"],
927
+ ["--publish", "portForward"]
928
+ ]);
929
+ var RESTORE_BARE_FLAGS = /* @__PURE__ */ new Map([["--lazy", "lazy"]]);
930
+ var RESTORE_FLAG_HANDLERS = {
931
+ lazy: handleRestoreLazy,
932
+ name: handleRestoreName,
933
+ image: handleRestoreImage,
934
+ liveMount: handleRestoreLiveMount,
935
+ portForward: handleRestorePortForward
936
+ };
822
937
  function parseRestoreArgs(argv) {
823
- const positional = [];
824
- let name;
825
- let image;
826
- let lazy = false;
827
- const portForward = [];
828
- const liveMounts = [];
829
- const seenLiveGuests = /* @__PURE__ */ new Set();
830
- const seenHostPorts = /* @__PURE__ */ new Set();
938
+ const state = newRestoreParseState();
831
939
  for (let i = 0; i < argv.length; i++) {
832
- const a = argv[i];
833
- if (a === "--lazy") {
834
- lazy = true;
835
- } else if (a === "--name" || a.startsWith("--name=")) {
836
- const v = a === "--name" ? argv[++i] : a.slice("--name=".length);
837
- if (!v) {
838
- throw new ParseError3("PARSE_FLAG_MISSING_VALUE", "--name requires a value");
839
- }
840
- if (name !== void 0) {
841
- throw new ParseError3(
842
- "PARSE_FLAG_DUPLICATE",
843
- "--name may be given at most once per invocation"
844
- );
845
- }
846
- name = v;
847
- } else if (a === "--image" || a.startsWith("--image=")) {
848
- const v = a === "--image" ? argv[++i] : a.slice("--image=".length);
849
- if (!v) {
850
- throw new ParseError3("PARSE_FLAG_MISSING_VALUE", "--image requires a value");
851
- }
852
- if (image !== void 0) {
853
- throw new ParseError3(
854
- "PARSE_FLAG_DUPLICATE",
855
- "--image may be given at most once per invocation"
856
- );
857
- }
858
- image = v;
859
- } else if (a === "--mount-live" || a.startsWith("--mount-live=")) {
860
- const { value, next } = consumeLiveMount(a, argv, i);
861
- i = next;
862
- if (seenLiveGuests.has(value.guest)) {
863
- throw new ParseError3(
864
- "PARSE_FLAG_DUPLICATE",
865
- `--mount-live override for guest=${value.guest} given more than once`
866
- );
867
- }
868
- seenLiveGuests.add(value.guest);
869
- liveMounts.push(value);
870
- } else if (a === "-p" || a === "--publish" || a.startsWith("-p=") || a.startsWith("--publish=")) {
871
- i = consumePortForward(a, argv, i, seenHostPorts, portForward);
872
- } else if (a.startsWith("-")) {
873
- throw new ParseError3("PARSE_FLAG_UNKNOWN", `unknown flag: ${a}`);
874
- } else {
875
- positional.push(a);
940
+ const arg = argv[i];
941
+ const flag = restoreFlagFor(arg);
942
+ if (flag) {
943
+ i = RESTORE_FLAG_HANDLERS[flag](state, arg, argv, i);
944
+ continue;
876
945
  }
946
+ if (arg.startsWith("-")) {
947
+ throw new ParseError3("PARSE_FLAG_UNKNOWN", `unknown flag: ${arg}`);
948
+ }
949
+ state.positional.push(arg);
950
+ }
951
+ return finishRestoreArgs(state);
952
+ }
953
+ function newRestoreParseState() {
954
+ return {
955
+ positional: [],
956
+ portForward: [],
957
+ lazy: false,
958
+ liveMounts: [],
959
+ seenLiveGuests: /* @__PURE__ */ new Set(),
960
+ seenHostPorts: /* @__PURE__ */ new Set()
961
+ };
962
+ }
963
+ function restoreFlagFor(arg) {
964
+ const eq = arg.indexOf("=");
965
+ if (eq !== -1) {
966
+ return RESTORE_VALUE_FLAGS.get(arg.slice(0, eq));
967
+ }
968
+ return RESTORE_BARE_FLAGS.get(arg) ?? RESTORE_VALUE_FLAGS.get(arg);
969
+ }
970
+ function finishRestoreArgs(state) {
971
+ return {
972
+ positional: state.positional,
973
+ name: state.name,
974
+ image: state.image,
975
+ portForward: state.portForward,
976
+ lazy: state.lazy,
977
+ liveMounts: state.liveMounts
978
+ };
979
+ }
980
+ function handleRestoreLazy(state, _flag, _args, index) {
981
+ state.lazy = true;
982
+ return index;
983
+ }
984
+ function handleRestoreName(state, flag, args, index) {
985
+ const { spec, next } = takeRestoreValue(flag, args, index, "a value", "--name");
986
+ assertRestoreFlagUnused(state.name !== void 0, "--name");
987
+ state.name = spec;
988
+ return next;
989
+ }
990
+ function handleRestoreImage(state, flag, args, index) {
991
+ const { spec, next } = takeRestoreValue(flag, args, index, "a value", "--image");
992
+ assertRestoreFlagUnused(state.image !== void 0, "--image");
993
+ state.image = spec;
994
+ return next;
995
+ }
996
+ function handleRestoreLiveMount(state, flag, args, index) {
997
+ const { value, next } = consumeLiveMount(flag, args, index);
998
+ assertRestoreLiveGuestUnused(state, value.guest);
999
+ state.seenLiveGuests.add(value.guest);
1000
+ state.liveMounts.push(value);
1001
+ return next;
1002
+ }
1003
+ function handleRestorePortForward(state, flag, args, index) {
1004
+ return consumePortForward(flag, args, index, state.seenHostPorts, state.portForward);
1005
+ }
1006
+ function takeRestoreValue(flag, args, index, label, displayFlag) {
1007
+ const result = takeValue(flag, args, index, label);
1008
+ if (!result.spec) {
1009
+ throw new ParseError3("PARSE_FLAG_MISSING_VALUE", `${displayFlag} requires ${label}`);
1010
+ }
1011
+ return result;
1012
+ }
1013
+ function assertRestoreFlagUnused(used, flag) {
1014
+ if (used) {
1015
+ throw new ParseError3(
1016
+ "PARSE_FLAG_DUPLICATE",
1017
+ `${flag} may be given at most once per invocation`
1018
+ );
1019
+ }
1020
+ }
1021
+ function assertRestoreLiveGuestUnused(state, guest) {
1022
+ if (state.seenLiveGuests.has(guest)) {
1023
+ throw new ParseError3(
1024
+ "PARSE_FLAG_DUPLICATE",
1025
+ `--mount-live override for guest=${guest} given more than once`
1026
+ );
877
1027
  }
878
- return { positional, name, image, portForward, lazy, liveMounts };
879
1028
  }
880
1029
 
881
1030
  // src/parse-target.ts
@@ -1095,37 +1244,69 @@ function printDiagnostics(summary, opts = {}) {
1095
1244
  if (!isQuiet()) {
1096
1245
  return;
1097
1246
  }
1098
- const bufStr = typeof opts.buffer === "string" ? opts.buffer : opts.buffer?.toString() ?? "";
1247
+ const buffer = diagnosticsBuffer(opts.buffer);
1099
1248
  const tails = opts.tails ?? {};
1100
- const hasBuf = bufStr.trim().length > 0;
1101
- const hasTails = Object.values(tails).some((t) => t && t.trim().length > 0);
1102
- if (hasBuf || hasTails) {
1103
- process.stderr.write("\n--- diagnostics ---\n");
1104
- if (hasBuf) {
1105
- process.stderr.write(bufStr);
1106
- if (!bufStr.endsWith("\n")) {
1107
- process.stderr.write("\n");
1108
- }
1249
+ printDiagnosticsEnvelope(buffer, tails);
1250
+ printDiagnosticsHint(opts.hint);
1251
+ }
1252
+ function diagnosticsBuffer(buffer) {
1253
+ if (typeof buffer === "string") {
1254
+ return buffer;
1255
+ }
1256
+ if (buffer === void 0) {
1257
+ return "";
1258
+ }
1259
+ return buffer.toString();
1260
+ }
1261
+ function printDiagnosticsEnvelope(buffer, tails) {
1262
+ const hasBuffer = hasDiagnosticsContent(buffer);
1263
+ if (!hasBuffer && !hasDiagnosticsTails(tails)) {
1264
+ return;
1265
+ }
1266
+ process.stderr.write("\n--- diagnostics ---\n");
1267
+ printDiagnosticsBuffer(buffer, hasBuffer);
1268
+ printDiagnosticsTails(tails, hasBuffer);
1269
+ process.stderr.write("--------------------\n\n");
1270
+ }
1271
+ function hasDiagnosticsContent(content) {
1272
+ if (!content) {
1273
+ return false;
1274
+ }
1275
+ return content.trim().length > 0;
1276
+ }
1277
+ function hasDiagnosticsTails(tails) {
1278
+ return Object.values(tails).some(hasDiagnosticsContent);
1279
+ }
1280
+ function printDiagnosticsBuffer(buffer, hasBuffer) {
1281
+ if (!hasBuffer) {
1282
+ return;
1283
+ }
1284
+ process.stderr.write(buffer);
1285
+ writeTrailingNewline(buffer);
1286
+ }
1287
+ function printDiagnosticsTails(tails, afterBuffer) {
1288
+ for (const [label, content] of Object.entries(tails)) {
1289
+ if (!hasDiagnosticsContent(content)) {
1290
+ continue;
1109
1291
  }
1110
- for (const [label, content] of Object.entries(tails)) {
1111
- if (!content || content.trim().length === 0) {
1112
- continue;
1113
- }
1114
- if (hasBuf) {
1115
- process.stderr.write("\n");
1116
- }
1117
- process.stderr.write(`[${label}]
1118
- `);
1119
- process.stderr.write(content);
1120
- if (!content.endsWith("\n")) {
1121
- process.stderr.write("\n");
1122
- }
1292
+ if (afterBuffer) {
1293
+ process.stderr.write("\n");
1123
1294
  }
1124
- process.stderr.write("--------------------\n\n");
1295
+ process.stderr.write(`[${label}]
1296
+ `);
1297
+ process.stderr.write(content);
1298
+ writeTrailingNewline(content);
1299
+ }
1300
+ }
1301
+ function writeTrailingNewline(content) {
1302
+ if (!content.endsWith("\n")) {
1303
+ process.stderr.write("\n");
1125
1304
  }
1126
- const hint = opts.hint ?? ESCAPE_HINT;
1127
- if (hint.length > 0) {
1128
- process.stderr.write(hint);
1305
+ }
1306
+ function printDiagnosticsHint(hint) {
1307
+ const text = hint ?? ESCAPE_HINT;
1308
+ if (text.length > 0) {
1309
+ process.stderr.write(text);
1129
1310
  }
1130
1311
  }
1131
1312
 
@@ -1155,51 +1336,93 @@ var CACHE_ROOT = join2(homedir2(), ".machinen");
1155
1336
  function cacheDirFor(tag) {
1156
1337
  return join2(CACHE_ROOT, tag);
1157
1338
  }
1158
- function baseDirFor(tag, distro = "debian", cpu = "arm64") {
1339
+ function guestCpu() {
1340
+ const override = process.env.MACHINEN_GUEST_ARCH;
1341
+ if (override === "arm64" || override === "amd64") {
1342
+ return override;
1343
+ }
1344
+ return osArch() === "x64" ? "amd64" : "arm64";
1345
+ }
1346
+ function baseAssetSpec() {
1347
+ return guestCpu() === "amd64" ? {
1348
+ cpu: "amd64",
1349
+ kernelAsset: "bzImage-x86_64",
1350
+ rootfsAsset: "rootfs-debian-amd64.tar.gz"
1351
+ } : {
1352
+ cpu: "arm64",
1353
+ kernelAsset: "Image-arm64",
1354
+ dtbAsset: "virt-arm64.dtb",
1355
+ rootfsAsset: "rootfs-debian-arm64.tar.gz"
1356
+ };
1357
+ }
1358
+ function baseDirFor(tag, distro = "debian", cpu = guestCpu()) {
1159
1359
  return join2(cacheDirFor(tag), "bases", `${distro}-${cpu}`);
1160
1360
  }
1161
1361
  function baseAssetsComplete(tag) {
1162
- const base = baseDirFor(tag);
1163
- return existsSync2(join2(base, "Image")) && existsSync2(join2(base, "virt.dtb")) && existsSync2(join2(base, "rootfs.tar.gz"));
1362
+ const spec = baseAssetSpec();
1363
+ const base = baseDirFor(tag, "debian", spec.cpu);
1364
+ return existsSync2(join2(base, "Image")) && (!spec.dtbAsset || existsSync2(join2(base, "virt.dtb"))) && existsSync2(join2(base, "rootfs.tar.gz"));
1164
1365
  }
1165
- var ASSETS_DIR_FILES = ["Image-arm64", "virt-arm64.dtb", "rootfs-debian-arm64.tar.gz"];
1166
1366
  function validateAssetsDir(dir) {
1167
1367
  const abs = resolve(dir);
1168
1368
  if (!existsSync2(abs)) {
1169
1369
  die(`MACHINEN_ASSETS_DIR=${dir} does not exist`);
1170
1370
  }
1171
- const missing = ASSETS_DIR_FILES.filter((f) => !existsSync2(join2(abs, f)));
1371
+ const spec = baseAssetSpec();
1372
+ const required = [spec.kernelAsset, spec.rootfsAsset, ...spec.dtbAsset ? [spec.dtbAsset] : []];
1373
+ const missing = required.filter((f) => !existsSync2(join2(abs, f)));
1172
1374
  if (missing.length > 0) {
1173
1375
  die(
1174
- `MACHINEN_ASSETS_DIR=${dir} is missing: ${missing.join(", ")}
1376
+ `MACHINEN_ASSETS_DIR=${dir} is missing for ${spec.cpu}: ${missing.join(", ")}
1175
1377
  Produce them with ./scripts/build-base-assets.sh (outputs to ./release-assets/).`
1176
1378
  );
1177
1379
  }
1178
1380
  }
1179
1381
  async function ensureBaseAssets(tag) {
1180
- const base = baseDirFor(tag);
1181
- const kernel = join2(base, "Image");
1182
- const dtb = join2(base, "virt.dtb");
1183
- const tarball = join2(base, "rootfs.tar.gz");
1184
- if (existsSync2(kernel) && existsSync2(dtb) && existsSync2(tarball)) {
1382
+ const spec = baseAssetSpec();
1383
+ const base = baseDirFor(tag, "debian", spec.cpu);
1384
+ if (cachedBaseAssetsReady(base, spec)) {
1185
1385
  return base;
1186
1386
  }
1187
1387
  mkdirSync2(base, { recursive: true });
1188
- const assets = [
1189
- { name: "Image-arm64", dest: kernel },
1190
- { name: "virt-arm64.dtb", dest: dtb },
1191
- { name: "rootfs-debian-arm64.tar.gz", dest: tarball }
1192
- ];
1193
- await Promise.all(assets.map((a) => downloadWithChecksum(a.name, a.dest, tag)));
1388
+ await downloadBaseAssets(tag, base, spec);
1389
+ replaceCurrentBaseSymlink(tag);
1390
+ return base;
1391
+ }
1392
+ function cachedBaseAssetsReady(base, spec) {
1393
+ if (!existsSync2(join2(base, "Image"))) {
1394
+ return false;
1395
+ }
1396
+ if (spec.dtbAsset && !existsSync2(join2(base, "virt.dtb"))) {
1397
+ return false;
1398
+ }
1399
+ return existsSync2(join2(base, "rootfs.tar.gz"));
1400
+ }
1401
+ async function downloadBaseAssets(tag, base, spec) {
1402
+ await Promise.all(
1403
+ baseAssetDownloads(base, spec).map((a) => downloadWithChecksum(a.name, a.dest, tag))
1404
+ );
1405
+ }
1406
+ function baseAssetDownloads(base, spec) {
1407
+ const assets = [{ name: spec.kernelAsset, dest: join2(base, "Image") }];
1408
+ if (spec.dtbAsset) {
1409
+ assets.push({ name: spec.dtbAsset, dest: join2(base, "virt.dtb") });
1410
+ }
1411
+ assets.push({ name: spec.rootfsAsset, dest: join2(base, "rootfs.tar.gz") });
1412
+ return assets;
1413
+ }
1414
+ function replaceCurrentBaseSymlink(tag) {
1194
1415
  const current = join2(CACHE_ROOT, "current");
1195
1416
  try {
1196
- if (existsSync2(current) || isSymlink(current)) {
1197
- unlinkSync(current);
1198
- }
1417
+ unlinkCurrentSymlink(current);
1199
1418
  } catch {
1200
1419
  }
1201
1420
  symlinkSync(tag, current, "dir");
1202
- return base;
1421
+ }
1422
+ function unlinkCurrentSymlink(current) {
1423
+ if (existsSync2(current) || isSymlink(current)) {
1424
+ unlinkSync(current);
1425
+ }
1203
1426
  }
1204
1427
  function isSymlink(p) {
1205
1428
  try {
@@ -1343,36 +1566,7 @@ function emitJson(value) {
1343
1566
  function emitJsonError(code, message) {
1344
1567
  process.stderr.write(JSON.stringify({ schema_version: 1, error: { code, message } }) + "\n");
1345
1568
  }
1346
- async function cmdBoot(args) {
1347
- let parsed;
1348
- try {
1349
- parsed = parseRunArgs(args);
1350
- } catch (err) {
1351
- handleError(err);
1352
- }
1353
- const {
1354
- positional,
1355
- double_dash_args,
1356
- mount,
1357
- liveMounts,
1358
- env,
1359
- portForward,
1360
- snapshot,
1361
- name,
1362
- guestCwd,
1363
- detached,
1364
- memory,
1365
- json
1366
- } = parsed;
1367
- if (json && !detached) {
1368
- die("boot --json is only meaningful with --detach (attached boots take over stdio).");
1369
- }
1370
- if (positional.length > 1) {
1371
- die(
1372
- "usage: machinen boot [<image>] [--snapshot <path>] [--name <name>] [--cwd <abs-path>] [--mount ...] [--mount-live ...] [--env KEY=VALUE]... [--detached] [--memory <mib>] [-- <cmd> [args...]]"
1373
- );
1374
- }
1375
- const imageOverride = positional[0];
1569
+ async function resolveCliBaseAssets() {
1376
1570
  const assetsOverride = process.env.MACHINEN_ASSETS_DIR;
1377
1571
  if (assetsOverride) {
1378
1572
  validateAssetsDir(assetsOverride);
@@ -1381,311 +1575,462 @@ async function cmdBoot(args) {
1381
1575
  `);
1382
1576
  await ensureBaseAssets(RELEASE_TAG);
1383
1577
  }
1384
- const baseDir = assetsOverride ? resolve(assetsOverride) : baseDirFor(RELEASE_TAG);
1385
- const kernelPath = join2(baseDir, assetsOverride ? "Image-arm64" : "Image");
1386
- const dtbPath = join2(baseDir, assetsOverride ? "virt-arm64.dtb" : "virt.dtb");
1387
- const defaultImagePath = join2(
1388
- baseDir,
1389
- assetsOverride ? "rootfs-debian-arm64.tar.gz" : "rootfs.tar.gz"
1390
- );
1391
- const imagePath = imageOverride ? resolve(imageOverride) : defaultImagePath;
1392
- debug(
1393
- "boot baseDir=%s kernel=%s dtb=%s image=%s snapshot=%s name=%s",
1578
+ return cliBaseAssetPaths(assetsOverride);
1579
+ }
1580
+ function cliBaseAssetPaths(assetsOverride) {
1581
+ const spec = baseAssetSpec();
1582
+ const baseDir = cliBaseDir(assetsOverride, spec.cpu);
1583
+ return {
1394
1584
  baseDir,
1395
- kernelPath,
1396
- dtbPath,
1397
- imagePath,
1398
- snapshot ?? "<none>",
1399
- name ?? "<unset>"
1400
- );
1401
- const cmd = double_dash_args.length > 0 ? ["/usr/bin/env", ...double_dash_args] : void 0;
1402
- const headlineName = name ?? deriveBootName(imageOverride);
1403
- const showHeadlines = isQuiet() && !(detached && json);
1404
- const bootT0 = Date.now();
1405
- const buffer = new RingBuffer();
1406
- let filter = null;
1407
- let filterOut = null;
1408
- if (showHeadlines) {
1409
- printHeadline(`booting ${headlineName}\u2026`);
1410
- if (!detached) {
1411
- filterOut = new PassThrough();
1412
- filter = new NoiseFilter({
1413
- buffer,
1414
- out: filterOut,
1415
- onReady: () => {
1416
- printHeadline("guest ready");
1417
- printHeadline(`ready in ${formatElapsed(Date.now() - bootT0)}`);
1418
- }
1419
- });
1420
- }
1585
+ kernelPath: cliKernelPath(baseDir, assetsOverride, spec),
1586
+ dtbPath: cliDtbPath(baseDir, assetsOverride, spec),
1587
+ defaultImagePath: cliRootfsPath(baseDir, assetsOverride, spec)
1588
+ };
1589
+ }
1590
+ function cliBaseDir(assetsOverride, cpu) {
1591
+ if (assetsOverride) {
1592
+ return resolve(assetsOverride);
1421
1593
  }
1422
- const onLog = filter ? (evt) => {
1423
- if (evt.source === "guest-console") {
1424
- filter.push(evt.chunk);
1425
- }
1426
- } : showHeadlines ? (evt) => {
1427
- if (evt.source === "guest-console") {
1428
- buffer.push(evt.chunk);
1429
- }
1430
- } : void 0;
1431
- let vm;
1432
- try {
1433
- vm = await boot({
1434
- // Always pass the base rootfs so /sbin/machinen-restore and
1435
- // friends are in the initramfs even on a bare `machinen restore
1436
- // <snap>` (no --image, no -- cmd).
1437
- image: imagePath,
1438
- cmd,
1439
- env,
1440
- kernel: kernelPath,
1441
- dtb: dtbPath,
1442
- mount,
1443
- liveMounts,
1444
- portForward,
1445
- snapshot,
1446
- name,
1447
- guestCwd,
1448
- detached,
1449
- memory,
1450
- onLog,
1451
- // Interactive CLI: the session lives as long as the guest does.
1452
- // Don't impose the default 60s cap. Detached boots fall back to
1453
- // the runtime's own readiness timeout (60s) so the CLI can't
1454
- // hang forever waiting for first-guest-byte.
1455
- timeoutMs: detached ? void 0 : null
1456
- });
1457
- } catch (err) {
1458
- filter?.flush();
1459
- if (showHeadlines) {
1460
- failQuiet(`boot ${headlineName} failed: ${describeError(err)}`, {
1461
- buffer
1462
- });
1463
- }
1464
- handleError(err);
1594
+ return baseDirFor(RELEASE_TAG, "debian", cpu);
1595
+ }
1596
+ function cliKernelPath(baseDir, assetsOverride, spec) {
1597
+ return join2(baseDir, assetsOverride ? spec.kernelAsset : "Image");
1598
+ }
1599
+ function cliDtbPath(baseDir, assetsOverride, spec) {
1600
+ if (!spec.dtbAsset) {
1601
+ return void 0;
1465
1602
  }
1466
- if (detached) {
1467
- if (json) {
1468
- emitJson({
1469
- schema_version: 1,
1470
- pid: vm.pid,
1471
- name: name ?? null,
1472
- detached: true
1473
- });
1474
- } else {
1475
- const target = name ?? `pid ${vm.pid}`;
1476
- process.stderr.write(
1477
- `machinen: detached (${target}). Reattach: machinen attach ${name ?? vm.pid}
1478
- Stop: kill ${vm.pid} (machinen stop ships in PR2)
1479
- `
1480
- );
1481
- }
1482
- return 0;
1603
+ return join2(baseDir, assetsOverride ? spec.dtbAsset : "virt.dtb");
1604
+ }
1605
+ function cliRootfsPath(baseDir, assetsOverride, spec) {
1606
+ return join2(baseDir, assetsOverride ? spec.rootfsAsset : "rootfs.tar.gz");
1607
+ }
1608
+ function resolveOptionalImageOverride(imageOverride) {
1609
+ if (!imageOverride) {
1610
+ return void 0;
1611
+ }
1612
+ const imagePath = resolve(imageOverride);
1613
+ if (!existsSync2(imagePath)) {
1614
+ die(`--image: file not found: ${imagePath}`);
1483
1615
  }
1616
+ return imagePath;
1617
+ }
1618
+ async function runAttachedVmSession(vm, opts) {
1484
1619
  vm.stdout.pipe(process.stdout);
1485
- if (!filter) {
1620
+ if (!opts.filter) {
1486
1621
  vm.stderr.pipe(process.stderr);
1487
1622
  }
1488
1623
  const restoreStdin = rawModeStdinIfTTY();
1489
1624
  const cancelHintRepeat = printCtrlDHint();
1490
- let forwardedSignal = null;
1625
+ const signalState = installVmSignalHandlers(vm);
1626
+ pipeStdinToVm(vm.stdin, () => {
1627
+ process.stderr.write("\nmachinen: Ctrl-D \u2014 stopping VM\n");
1628
+ signalState.forwardedSignal = "SIGTERM";
1629
+ void vm.kill();
1630
+ });
1631
+ opts.filterOut?.pipe(process.stderr);
1632
+ try {
1633
+ return await waitForAttachedVm(vm, opts, signalState);
1634
+ } finally {
1635
+ signalState.remove();
1636
+ cancelHintRepeat();
1637
+ restoreStdin();
1638
+ }
1639
+ }
1640
+ function installVmSignalHandlers(vm) {
1641
+ const state = { forwardedSignal: null, remove: () => {
1642
+ } };
1491
1643
  const onSigint = () => {
1492
- forwardedSignal = "SIGINT";
1644
+ state.forwardedSignal = "SIGINT";
1493
1645
  void vm.kill();
1494
1646
  };
1495
1647
  const onSigterm = () => {
1496
- forwardedSignal = "SIGTERM";
1648
+ state.forwardedSignal = "SIGTERM";
1497
1649
  void vm.kill();
1498
1650
  };
1499
1651
  process.on("SIGINT", onSigint);
1500
1652
  process.on("SIGTERM", onSigterm);
1501
- pipeStdinToVm(vm.stdin, () => {
1502
- process.stderr.write("\nmachinen: Ctrl-D \u2014 stopping VM\n");
1503
- forwardedSignal = "SIGTERM";
1504
- void vm.kill();
1505
- });
1506
- filterOut?.pipe(process.stderr);
1507
- try {
1508
- const { code } = await vm.wait();
1509
- filter?.flush();
1510
- if (forwardedSignal === "SIGINT") {
1511
- return 130;
1512
- }
1513
- if (forwardedSignal === "SIGTERM") {
1514
- return 143;
1515
- }
1516
- if (filter && !filter.ready && code != null && code !== 0 && !forwardedSignal) {
1517
- printDiagnostics(`boot ${headlineName} exited ${code} before reaching ready`, { buffer });
1518
- }
1519
- return code ?? 0;
1520
- } finally {
1653
+ state.remove = () => {
1521
1654
  process.off("SIGINT", onSigint);
1522
1655
  process.off("SIGTERM", onSigterm);
1523
- cancelHintRepeat();
1524
- restoreStdin();
1656
+ };
1657
+ return state;
1658
+ }
1659
+ async function waitForAttachedVm(vm, opts, signalState) {
1660
+ const { code } = await vm.wait();
1661
+ opts.filter?.flush();
1662
+ const signalExitCode = forwardedSignalExitCode(signalState.forwardedSignal);
1663
+ if (signalExitCode !== void 0) {
1664
+ return signalExitCode;
1665
+ }
1666
+ if (shouldPrintPreReadyDiagnostics(opts.filter, code, signalState.forwardedSignal)) {
1667
+ printDiagnostics(opts.preReadyExitSummary(code), { buffer: opts.buffer });
1525
1668
  }
1669
+ return code ?? 0;
1526
1670
  }
1527
- async function cmdInstall(args) {
1528
- const { json, rest } = consumeJsonFlag(args);
1529
- const tag = argValue(rest, "--version") ?? RELEASE_TAG;
1530
- const wasComplete = baseAssetsComplete(tag);
1531
- const t0 = Date.now();
1532
- if (!json) {
1533
- process.stderr.write(`installing base assets for ${tag}\u2026
1534
- `);
1535
- if (!isQuiet()) {
1536
- process.stderr.write(` into ${cacheDirFor(tag)}
1537
- `);
1538
- }
1671
+ function forwardedSignalExitCode(signal) {
1672
+ if (signal === "SIGINT") {
1673
+ return 130;
1539
1674
  }
1540
- let base;
1541
- try {
1542
- base = await ensureBaseAssets(tag);
1543
- } catch (err) {
1544
- if (isQuiet() && !json) {
1545
- failQuiet(`install ${tag} failed: ${describeError(err)}`);
1546
- }
1547
- throw err;
1675
+ if (signal === "SIGTERM") {
1676
+ return 143;
1548
1677
  }
1549
- if (json) {
1550
- emitJson({
1551
- schema_version: 1,
1552
- tag,
1553
- base_dir: base,
1554
- fetched: !wasComplete
1555
- });
1556
- } else if (wasComplete) {
1557
- process.stderr.write(`ready: ${base} (cached)
1558
- `);
1559
- } else {
1560
- process.stderr.write(`ready in ${formatElapsed(Date.now() - t0)}: ${base}
1561
- `);
1678
+ return void 0;
1679
+ }
1680
+ function shouldPrintPreReadyDiagnostics(filter, code, forwardedSignal) {
1681
+ if (!filter) {
1682
+ return false;
1562
1683
  }
1563
- return 0;
1684
+ if (filter.ready || forwardedSignal) {
1685
+ return false;
1686
+ }
1687
+ return isNonZeroExit(code);
1564
1688
  }
1565
- async function cmdRestore(args) {
1566
- let parsed;
1689
+ function isNonZeroExit(code) {
1690
+ if (code === null) {
1691
+ return false;
1692
+ }
1693
+ return code !== 0;
1694
+ }
1695
+ async function cmdBoot(args) {
1696
+ const parsed = parseBootCommandArgs(args);
1697
+ validateBootCommandArgs(parsed);
1698
+ const imageOverride = parsed.positional[0];
1699
+ const paths = await resolveCliBaseAssets();
1700
+ const imagePath = imageOverride ? resolve(imageOverride) : paths.defaultImagePath;
1701
+ logBootPlan(paths, imagePath, parsed);
1702
+ const quiet = createBootQuietState(parsed, imageOverride);
1703
+ const vm = await startBootVm(parsed, paths, imagePath, bootEnvCommand(parsed), quiet);
1704
+ if (parsed.detached) {
1705
+ reportDetachedBoot(vm, parsed);
1706
+ return 0;
1707
+ }
1708
+ return runBootAttachedSession(vm, quiet);
1709
+ }
1710
+ function parseBootCommandArgs(args) {
1567
1711
  try {
1568
- parsed = parseRestoreArgs(args);
1712
+ return parseRunArgs(args);
1569
1713
  } catch (err) {
1570
1714
  handleError(err);
1571
1715
  }
1572
- const { positional, name, image: imageOverride, portForward, lazy, liveMounts } = parsed;
1573
- if (positional.length !== 1) {
1574
- die(
1575
- "usage: machinen restore <snap-dir> [--image <tarball>] [--name <name>] [--lazy] [-p <hostPort>:<guestPort>] [--mount-live <host>:<guest>[:<mode>]]"
1576
- );
1716
+ }
1717
+ function validateBootCommandArgs(parsed) {
1718
+ if (parsed.json && !parsed.detached) {
1719
+ die("boot --json is only meaningful with --detach (attached boots take over stdio).");
1577
1720
  }
1578
- const snapDir = resolve(positional[0]);
1579
- const assetsOverride = process.env.MACHINEN_ASSETS_DIR;
1580
- if (assetsOverride) {
1581
- validateAssetsDir(assetsOverride);
1582
- } else if (!baseAssetsComplete(RELEASE_TAG)) {
1583
- process.stderr.write(`machinen: fetching base assets for ${RELEASE_TAG} (first run)
1584
- `);
1585
- await ensureBaseAssets(RELEASE_TAG);
1721
+ if (parsed.positional.length > 1) {
1722
+ die(bootUsage());
1586
1723
  }
1587
- const baseDir = assetsOverride ? resolve(assetsOverride) : baseDirFor(RELEASE_TAG);
1588
- const kernelPath = join2(baseDir, assetsOverride ? "Image-arm64" : "Image");
1589
- const dtbPath = join2(baseDir, assetsOverride ? "virt-arm64.dtb" : "virt.dtb");
1590
- let imagePath;
1591
- if (imageOverride) {
1592
- imagePath = resolve(imageOverride);
1593
- if (!existsSync2(imagePath)) {
1594
- die(`--image: file not found: ${imagePath}`);
1595
- }
1724
+ }
1725
+ function bootUsage() {
1726
+ return "usage: machinen boot [<image>] [--snapshot <path>] [--name <name>] [--cwd <abs-path>] [--mount ...] [--mount-live ...] [--env KEY=VALUE]... [--detached] [--nested] [--memory <mib>] [-- <cmd> [args...]]";
1727
+ }
1728
+ function logBootPlan(paths, imagePath, parsed) {
1729
+ debug(
1730
+ "boot baseDir=%s kernel=%s dtb=%s image=%s snapshot=%s name=%s",
1731
+ paths.baseDir,
1732
+ paths.kernelPath,
1733
+ paths.dtbPath,
1734
+ imagePath,
1735
+ parsed.snapshot ?? "<none>",
1736
+ parsed.name ?? "<unset>"
1737
+ );
1738
+ }
1739
+ function bootEnvCommand(parsed) {
1740
+ if (parsed.double_dash_args.length === 0) {
1741
+ return void 0;
1596
1742
  }
1597
- const headlineName = name ?? deriveBootName(snapDir);
1598
- const showHeadlines = isQuiet();
1599
- const restoreT0 = Date.now();
1743
+ return ["/usr/bin/env", ...parsed.double_dash_args];
1744
+ }
1745
+ function createBootQuietState(parsed, imageOverride) {
1746
+ const headlineName = parsed.name ?? deriveBootName(imageOverride);
1747
+ const showHeadlines = shouldShowBootHeadlines(parsed);
1600
1748
  const buffer = new RingBuffer();
1601
- let filter = null;
1602
- if (showHeadlines) {
1603
- printHeadline(`restoring ${headlineName}\u2026`);
1604
- filter = new NoiseFilter({
1605
- buffer,
1606
- out: process.stderr,
1607
- onReady: () => {
1608
- printHeadline(`restored in ${formatElapsed(Date.now() - restoreT0)}`);
1609
- }
1610
- });
1749
+ if (!showHeadlines) {
1750
+ return { headlineName, showHeadlines, buffer, filter: null, filterOut: null };
1611
1751
  }
1612
- const onLog = filter ? (evt) => {
1752
+ return createVisibleBootQuietState(parsed, headlineName, showHeadlines, buffer);
1753
+ }
1754
+ function shouldShowBootHeadlines(parsed) {
1755
+ if (!isQuiet()) {
1756
+ return false;
1757
+ }
1758
+ if (parsed.detached && parsed.json) {
1759
+ return false;
1760
+ }
1761
+ return true;
1762
+ }
1763
+ function createVisibleBootQuietState(parsed, headlineName, showHeadlines, buffer) {
1764
+ printHeadline(`booting ${headlineName}\u2026`);
1765
+ if (parsed.detached) {
1766
+ return bootBufferOnlyQuietState(headlineName, showHeadlines, buffer);
1767
+ }
1768
+ return bootFilteredQuietState(headlineName, showHeadlines, buffer, Date.now());
1769
+ }
1770
+ function bootBufferOnlyQuietState(headlineName, showHeadlines, buffer) {
1771
+ return {
1772
+ headlineName,
1773
+ showHeadlines,
1774
+ buffer,
1775
+ filter: null,
1776
+ filterOut: null,
1777
+ onLog: guestConsoleOnLog((chunk) => buffer.push(chunk))
1778
+ };
1779
+ }
1780
+ function bootFilteredQuietState(headlineName, showHeadlines, buffer, bootT0) {
1781
+ const filterOut = new PassThrough();
1782
+ const filter = new NoiseFilter({
1783
+ buffer,
1784
+ out: filterOut,
1785
+ onReady: () => {
1786
+ printHeadline("guest ready");
1787
+ printHeadline(`ready in ${formatElapsed(Date.now() - bootT0)}`);
1788
+ }
1789
+ });
1790
+ return {
1791
+ headlineName,
1792
+ showHeadlines,
1793
+ buffer,
1794
+ filter,
1795
+ filterOut,
1796
+ onLog: guestConsoleOnLog((chunk) => filter.push(chunk))
1797
+ };
1798
+ }
1799
+ function guestConsoleOnLog(push) {
1800
+ return (evt) => {
1613
1801
  if (evt.source === "guest-console") {
1614
- filter.push(evt.chunk);
1802
+ push(evt.chunk);
1615
1803
  }
1616
- } : void 0;
1617
- let vm;
1804
+ };
1805
+ }
1806
+ async function startBootVm(parsed, paths, imagePath, cmd, quiet) {
1618
1807
  try {
1619
- vm = await restore({
1620
- snapDir,
1808
+ return await boot({
1809
+ // Always pass the base rootfs so /sbin/machinen-restore and
1810
+ // friends are in the initramfs even on a bare `machinen restore
1811
+ // <snap>` (no --image, no -- cmd).
1621
1812
  image: imagePath,
1622
- kernel: kernelPath,
1623
- dtb: dtbPath,
1624
- name,
1625
- lazy,
1626
- portForward: portForward.length > 0 ? portForward : void 0,
1813
+ cmd,
1814
+ env: parsed.env,
1815
+ kernel: paths.kernelPath,
1816
+ dtb: paths.dtbPath,
1817
+ mount: parsed.mount,
1818
+ liveMounts: parsed.liveMounts,
1819
+ portForward: parsed.portForward,
1820
+ snapshot: parsed.snapshot,
1821
+ nested: parsed.nested,
1822
+ name: parsed.name,
1823
+ guestCwd: parsed.guestCwd,
1824
+ detached: parsed.detached,
1825
+ memory: parsed.memory,
1826
+ onLog: quiet.onLog,
1827
+ // Interactive CLI: the session lives as long as the guest does.
1828
+ // Don't impose the default 60s cap. Detached boots fall back to
1829
+ // the runtime's own readiness timeout (60s) so the CLI can't
1830
+ // hang forever waiting for first-guest-byte.
1831
+ timeoutMs: parsed.detached ? void 0 : null
1832
+ });
1833
+ } catch (err) {
1834
+ handleBootFailure(err, quiet);
1835
+ }
1836
+ }
1837
+ function handleBootFailure(err, quiet) {
1838
+ quiet.filter?.flush();
1839
+ if (quiet.showHeadlines) {
1840
+ failQuiet(`boot ${quiet.headlineName} failed: ${describeError(err)}`, {
1841
+ buffer: quiet.buffer
1842
+ });
1843
+ }
1844
+ handleError(err);
1845
+ }
1846
+ function reportDetachedBoot(vm, parsed) {
1847
+ if (parsed.json) {
1848
+ emitDetachedBootJson(vm, parsed);
1849
+ return;
1850
+ }
1851
+ printDetachedBootHint(vm, parsed);
1852
+ }
1853
+ function emitDetachedBootJson(vm, parsed) {
1854
+ emitJson({ schema_version: 1, pid: vm.pid, name: parsed.name ?? null, detached: true });
1855
+ }
1856
+ function printDetachedBootHint(vm, parsed) {
1857
+ const target = parsed.name ?? `pid ${vm.pid}`;
1858
+ process.stderr.write(
1859
+ `machinen: detached (${target}). Reattach: machinen attach ${parsed.name ?? vm.pid}
1860
+ Stop: kill ${vm.pid} (machinen stop ships in PR2)
1861
+ `
1862
+ );
1863
+ }
1864
+ function runBootAttachedSession(vm, quiet) {
1865
+ return runAttachedVmSession(vm, {
1866
+ filter: quiet.filter,
1867
+ filterOut: quiet.filterOut,
1868
+ buffer: quiet.buffer,
1869
+ preReadyExitSummary: (code) => `boot ${quiet.headlineName} exited ${code} before reaching ready`
1870
+ });
1871
+ }
1872
+ async function cmdInstall(args) {
1873
+ const opts = parseInstallOptions(args);
1874
+ const result = await installBaseAssets(opts);
1875
+ reportInstallResult(opts, result);
1876
+ return 0;
1877
+ }
1878
+ function parseInstallOptions(args) {
1879
+ const { json, rest } = consumeJsonFlag(args);
1880
+ return { json, tag: argValue(rest, "--version") ?? RELEASE_TAG };
1881
+ }
1882
+ async function installBaseAssets(opts) {
1883
+ const wasComplete = baseAssetsComplete(opts.tag);
1884
+ const t0 = Date.now();
1885
+ printInstallStart(opts);
1886
+ try {
1887
+ const base = await ensureBaseAssets(opts.tag);
1888
+ return { base, wasComplete, elapsedMs: Date.now() - t0 };
1889
+ } catch (err) {
1890
+ reportInstallFailure(opts, err);
1891
+ throw err;
1892
+ }
1893
+ }
1894
+ function printInstallStart(opts) {
1895
+ if (opts.json) {
1896
+ return;
1897
+ }
1898
+ process.stderr.write(`installing base assets for ${opts.tag}\u2026
1899
+ `);
1900
+ if (!isQuiet()) {
1901
+ process.stderr.write(` into ${cacheDirFor(opts.tag)}
1902
+ `);
1903
+ }
1904
+ }
1905
+ function reportInstallFailure(opts, err) {
1906
+ if (isQuiet() && !opts.json) {
1907
+ failQuiet(`install ${opts.tag} failed: ${describeError(err)}`);
1908
+ }
1909
+ }
1910
+ function reportInstallResult(opts, result) {
1911
+ if (opts.json) {
1912
+ emitInstallJson(opts, result);
1913
+ return;
1914
+ }
1915
+ printInstallReady(result);
1916
+ }
1917
+ function emitInstallJson(opts, result) {
1918
+ emitJson({
1919
+ schema_version: 1,
1920
+ tag: opts.tag,
1921
+ base_dir: result.base,
1922
+ fetched: !result.wasComplete
1923
+ });
1924
+ }
1925
+ function printInstallReady(result) {
1926
+ if (result.wasComplete) {
1927
+ process.stderr.write(`ready: ${result.base} (cached)
1928
+ `);
1929
+ return;
1930
+ }
1931
+ process.stderr.write(`ready in ${formatElapsed(result.elapsedMs)}: ${result.base}
1932
+ `);
1933
+ }
1934
+ async function cmdRestore(args) {
1935
+ const parsed = parseRestoreCommandArgs(args);
1936
+ validateRestoreCommandArgs(parsed);
1937
+ const snapDir = resolve(parsed.positional[0]);
1938
+ const paths = await resolveCliBaseAssets();
1939
+ const quiet = createRestoreQuietState(parsed, snapDir);
1940
+ const vm = await startRestoreVm(parsed, snapDir, paths, quiet);
1941
+ reportRestoreSuccess(vm, quiet);
1942
+ return runRestoreAttachedSession(vm, quiet);
1943
+ }
1944
+ function parseRestoreCommandArgs(args) {
1945
+ try {
1946
+ return parseRestoreArgs(args);
1947
+ } catch (err) {
1948
+ handleError(err);
1949
+ }
1950
+ }
1951
+ function validateRestoreCommandArgs(parsed) {
1952
+ if (parsed.positional.length !== 1) {
1953
+ die(restoreUsage());
1954
+ }
1955
+ }
1956
+ function restoreUsage() {
1957
+ return "usage: machinen restore <snap-dir> [--image <tarball>] [--name <name>] [--lazy] [-p <hostPort>:<guestPort>] [--mount-live <host>:<guest>[:<mode>]]";
1958
+ }
1959
+ function createRestoreQuietState(parsed, snapDir) {
1960
+ const headlineName = parsed.name ?? deriveBootName(snapDir);
1961
+ const buffer = new RingBuffer();
1962
+ if (!isQuiet()) {
1963
+ return { headlineName, showHeadlines: false, buffer, filter: null, filterOut: null };
1964
+ }
1965
+ printHeadline(`restoring ${headlineName}\u2026`);
1966
+ return restoreFilteredQuietState(headlineName, buffer, Date.now());
1967
+ }
1968
+ function restoreFilteredQuietState(headlineName, buffer, restoreT0) {
1969
+ const filter = new NoiseFilter({
1970
+ buffer,
1971
+ out: process.stderr,
1972
+ onReady: () => {
1973
+ printHeadline(`restored in ${formatElapsed(Date.now() - restoreT0)}`);
1974
+ }
1975
+ });
1976
+ return {
1977
+ headlineName,
1978
+ showHeadlines: true,
1979
+ buffer,
1980
+ filter,
1981
+ filterOut: null,
1982
+ onLog: guestConsoleOnLog((chunk) => filter.push(chunk))
1983
+ };
1984
+ }
1985
+ async function startRestoreVm(parsed, snapDir, paths, quiet) {
1986
+ try {
1987
+ return await restore({
1988
+ snapDir,
1989
+ image: resolveOptionalImageOverride(parsed.image),
1990
+ kernel: paths.kernelPath,
1991
+ dtb: paths.dtbPath,
1992
+ name: parsed.name,
1993
+ lazy: parsed.lazy,
1994
+ portForward: optionalList(parsed.portForward),
1627
1995
  // #273: per-guest overrides for the bundle's recorded
1628
1996
  // liveMounts. Empty list = use the bundle's recorded mounts
1629
1997
  // verbatim; non-empty entries replace the matching guest's
1630
1998
  // host/mode (BOOT_LIVE_MOUNT_OVERRIDE_UNKNOWN if no match).
1631
- liveMounts: liveMounts.length > 0 ? liveMounts : void 0,
1999
+ liveMounts: optionalList(parsed.liveMounts),
1632
2000
  timeoutMs: null,
1633
- onLog
2001
+ onLog: quiet.onLog
1634
2002
  });
1635
2003
  } catch (err) {
1636
- filter?.flush();
1637
- if (showHeadlines) {
1638
- failQuiet(`restore ${headlineName} failed: ${describeError(err)}`, {
1639
- buffer
1640
- });
1641
- }
1642
- handleError(err);
2004
+ handleRestoreFailure(err, quiet);
1643
2005
  }
1644
- if (!showHeadlines) {
2006
+ }
2007
+ function optionalList(items) {
2008
+ if (items.length === 0) {
2009
+ return void 0;
2010
+ }
2011
+ return items;
2012
+ }
2013
+ function handleRestoreFailure(err, quiet) {
2014
+ quiet.filter?.flush();
2015
+ if (quiet.showHeadlines) {
2016
+ failQuiet(`restore ${quiet.headlineName} failed: ${describeError(err)}`, {
2017
+ buffer: quiet.buffer
2018
+ });
2019
+ }
2020
+ handleError(err);
2021
+ }
2022
+ function reportRestoreSuccess(vm, quiet) {
2023
+ if (!quiet.showHeadlines) {
1645
2024
  process.stderr.write(`restored as: ${vm.name ?? "<anonymous>"} (pid ${vm.pid})
1646
2025
  `);
1647
2026
  }
1648
- vm.stdout.pipe(process.stdout);
1649
- if (!filter) {
1650
- vm.stderr.pipe(process.stderr);
1651
- }
1652
- const restoreStdin = rawModeStdinIfTTY();
1653
- const cancelHintRepeat = printCtrlDHint();
1654
- let forwardedSignal = null;
1655
- const onSigint = () => {
1656
- forwardedSignal = "SIGINT";
1657
- void vm.kill();
1658
- };
1659
- const onSigterm = () => {
1660
- forwardedSignal = "SIGTERM";
1661
- void vm.kill();
1662
- };
1663
- process.on("SIGINT", onSigint);
1664
- process.on("SIGTERM", onSigterm);
1665
- pipeStdinToVm(vm.stdin, () => {
1666
- process.stderr.write("\nmachinen: Ctrl-D \u2014 stopping VM\n");
1667
- forwardedSignal = "SIGTERM";
1668
- void vm.kill();
2027
+ }
2028
+ function runRestoreAttachedSession(vm, quiet) {
2029
+ return runAttachedVmSession(vm, {
2030
+ filter: quiet.filter,
2031
+ buffer: quiet.buffer,
2032
+ preReadyExitSummary: (code) => `restore ${quiet.headlineName} exited ${code} before reaching ready`
1669
2033
  });
1670
- try {
1671
- const { code } = await vm.wait();
1672
- filter?.flush();
1673
- if (forwardedSignal === "SIGINT") {
1674
- return 130;
1675
- }
1676
- if (forwardedSignal === "SIGTERM") {
1677
- return 143;
1678
- }
1679
- if (filter && !filter.ready && code != null && code !== 0 && !forwardedSignal) {
1680
- printDiagnostics(`restore ${headlineName} exited ${code} before reaching ready`, { buffer });
1681
- }
1682
- return code ?? 0;
1683
- } finally {
1684
- process.off("SIGINT", onSigint);
1685
- process.off("SIGTERM", onSigterm);
1686
- cancelHintRepeat();
1687
- restoreStdin();
1688
- }
1689
2034
  }
1690
2035
  async function cmdLs(args) {
1691
2036
  const { json, rest } = consumeJsonFlag(args);
@@ -1693,52 +2038,95 @@ async function cmdLs(args) {
1693
2038
  die(`unknown argument: ${rest[0]}`);
1694
2039
  }
1695
2040
  const entries = list();
1696
- const rssByPid = readHostRssBytesMulti(
1697
- entries.map((e) => ({ pid: e.pid, statsPath: e.statsPath }))
1698
- );
2041
+ const rssByPid = rssByRegistryPid(entries);
1699
2042
  if (json) {
1700
- emitJson({
1701
- schema_version: 1,
1702
- vms: entries.map((e) => ({
1703
- pid: e.pid,
1704
- name: e.name ?? null,
1705
- started_at: e.startedAt,
1706
- uptime_ms: Date.now() - e.startedAt,
1707
- memory: {
1708
- rss_bytes: rssByPid.get(e.pid) ?? null,
1709
- ceiling_mib: e.memoryCeilingMib ?? null
1710
- },
1711
- ports: e.portForward ?? [],
1712
- forked_from: e.forkedFrom ?? null
1713
- }))
1714
- });
1715
- return 0;
2043
+ emitLsJson(entries, rssByPid);
2044
+ } else {
2045
+ printLsTable(entries, rssByPid);
2046
+ }
2047
+ return 0;
2048
+ }
2049
+ function rssByRegistryPid(entries) {
2050
+ return readHostRssBytesMulti(
2051
+ entries.map((entry) => ({ pid: entry.pid, statsPath: entry.statsPath }))
2052
+ );
2053
+ }
2054
+ function emitLsJson(entries, rssByPid) {
2055
+ emitJson({
2056
+ schema_version: 1,
2057
+ vms: entries.map((entry) => vmJson(entry, rssByPid))
2058
+ });
2059
+ }
2060
+ function vmJson(entry, rssByPid) {
2061
+ return {
2062
+ pid: entry.pid,
2063
+ name: nullable(entry.name),
2064
+ started_at: entry.startedAt,
2065
+ uptime_ms: Date.now() - entry.startedAt,
2066
+ memory: vmMemoryJson(entry, rssByPid),
2067
+ ports: portsJson(entry),
2068
+ forked_from: nullable(entry.forkedFrom)
2069
+ };
2070
+ }
2071
+ function portsJson(entry) {
2072
+ if (entry.portForward === void 0) {
2073
+ return [];
1716
2074
  }
2075
+ return entry.portForward;
2076
+ }
2077
+ function vmMemoryJson(entry, rssByPid) {
2078
+ return {
2079
+ rss_bytes: nullable(rssByPid.get(entry.pid)),
2080
+ ceiling_mib: nullable(entry.memoryCeilingMib)
2081
+ };
2082
+ }
2083
+ function nullable(value) {
2084
+ if (value === void 0) {
2085
+ return null;
2086
+ }
2087
+ return value;
2088
+ }
2089
+ function printLsTable(entries, rssByPid) {
1717
2090
  if (entries.length === 0) {
1718
2091
  process.stdout.write("(no running VMs)\n");
1719
- return 0;
2092
+ return;
1720
2093
  }
1721
2094
  const header = ["PID", "NAME", "UP", "MEM", "PORTS", "FORKED-FROM"];
1722
- const rows = entries.map((e) => [
1723
- String(e.pid),
1724
- e.name ?? "-",
1725
- formatUptime(Date.now() - e.startedAt),
1726
- formatMem(rssByPid.get(e.pid) ?? null, e.memoryCeilingMib),
1727
- formatPorts(e.portForward),
1728
- e.forkedFrom ?? "-"
2095
+ const rows = lsRows(entries, rssByPid);
2096
+ const widths = tableWidths(header, rows);
2097
+ const visible = visibleLsColumns(header, widths);
2098
+ printTable(header, rows, widths, visible);
2099
+ }
2100
+ function lsRows(entries, rssByPid) {
2101
+ return entries.map((entry) => [
2102
+ String(entry.pid),
2103
+ entry.name ?? "-",
2104
+ formatUptime(Date.now() - entry.startedAt),
2105
+ formatMem(rssByPid.get(entry.pid) ?? null, entry.memoryCeilingMib),
2106
+ formatPorts(entry.portForward),
2107
+ entry.forkedFrom ?? "-"
1729
2108
  ]);
1730
- const widths = header.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2109
+ }
2110
+ function tableWidths(header, rows) {
2111
+ return header.map(
2112
+ (heading, index) => Math.max(heading.length, ...rows.map((row) => row[index].length))
2113
+ );
2114
+ }
2115
+ function visibleLsColumns(header, widths) {
1731
2116
  const gap = " ";
1732
- const fullWidth = widths.reduce((sum, w) => sum + w, 0) + gap.length * (widths.length - 1);
2117
+ const fullWidth = widths.reduce((sum, width) => sum + width, 0) + gap.length * (widths.length - 1);
1733
2118
  const cols = process.stdout.columns;
1734
2119
  const includeMem = cols === void 0 || fullWidth <= cols;
1735
- const visible = includeMem ? header.map((_, i) => i) : header.map((_, i) => i).filter((i) => i !== 3);
1736
- const line = (cells) => visible.map((i) => cells[i].padEnd(widths[i])).join(gap);
1737
- process.stdout.write(line(header) + "\n");
2120
+ return includeMem ? header.map((_, i) => i) : header.map((_, i) => i).filter((i) => i !== 3);
2121
+ }
2122
+ function printTable(header, rows, widths, visible) {
2123
+ process.stdout.write(formatTableLine(header, widths, visible) + "\n");
1738
2124
  for (const row of rows) {
1739
- process.stdout.write(line(row) + "\n");
2125
+ process.stdout.write(formatTableLine(row, widths, visible) + "\n");
1740
2126
  }
1741
- return 0;
2127
+ }
2128
+ function formatTableLine(cells, widths, visible) {
2129
+ return visible.map((index) => cells[index].padEnd(widths[index])).join(" ");
1742
2130
  }
1743
2131
  function formatUptime(ms) {
1744
2132
  const s = Math.floor(ms / 1e3);
@@ -1756,180 +2144,265 @@ function formatUptime(ms) {
1756
2144
  return `${Math.floor(h / 24)}d`;
1757
2145
  }
1758
2146
  async function cmdGc(args) {
1759
- const { json, rest: afterJson } = consumeJsonFlag(args);
1760
- const { dryRun, rest } = consumeDryRunFlag(afterJson);
1761
- for (const a of rest) {
1762
- die(`unknown flag: ${a}`);
1763
- }
2147
+ const { json, dryRun, rest } = parseGcOptions(args);
2148
+ dieOnUnexpectedArgs(rest);
1764
2149
  const results = runGc({ dryRun });
1765
2150
  if (json) {
1766
- emitJson({
1767
- schema_version: 1,
1768
- dry_run: dryRun,
1769
- results: results.map((r) => ({
1770
- pid: r.pid,
1771
- name: r.name ?? null,
1772
- status: r.status,
1773
- removed_paths: r.removedPaths,
1774
- failed_paths: r.failedPaths
1775
- }))
1776
- });
1777
- return 0;
2151
+ emitGcJson(dryRun, results);
2152
+ } else {
2153
+ printGcResults(results, dryRun);
1778
2154
  }
2155
+ return 0;
2156
+ }
2157
+ function parseGcOptions(args) {
2158
+ const { json, rest: afterJson } = consumeJsonFlag(args);
2159
+ const { dryRun, rest } = consumeDryRunFlag(afterJson);
2160
+ return { json, dryRun, rest };
2161
+ }
2162
+ function dieOnUnexpectedArgs(args) {
2163
+ for (const arg of args) {
2164
+ die(`unknown flag: ${arg}`);
2165
+ }
2166
+ }
2167
+ function emitGcJson(dryRun, results) {
2168
+ emitJson({
2169
+ schema_version: 1,
2170
+ dry_run: dryRun,
2171
+ results: results.map((r) => ({
2172
+ pid: r.pid,
2173
+ name: r.name ?? null,
2174
+ status: r.status,
2175
+ removed_paths: r.removedPaths,
2176
+ failed_paths: r.failedPaths
2177
+ }))
2178
+ });
2179
+ }
2180
+ function printGcResults(results, dryRun) {
1779
2181
  if (results.length === 0) {
1780
2182
  process.stdout.write("(nothing to clean up)\n");
1781
- return 0;
2183
+ return;
1782
2184
  }
1783
- for (const r of results) {
1784
- const label = r.name ? `${r.name} (pid ${r.pid})` : `pid ${r.pid}`;
1785
- const verb = dryRun ? "would clean" : "cleaned";
1786
- process.stdout.write(`${verb} ${label} [${r.status}]: ${r.removedPaths.length} path(s)
1787
- `);
1788
- for (const p of r.removedPaths) {
1789
- process.stdout.write(` ${p}
1790
- `);
1791
- }
1792
- for (const p of r.failedPaths) {
1793
- process.stdout.write(` failed: ${p}
2185
+ for (const result of results) {
2186
+ printGcResult(result, dryRun);
2187
+ }
2188
+ }
2189
+ function printGcResult(result, dryRun) {
2190
+ const label = result.name ? `${result.name} (pid ${result.pid})` : `pid ${result.pid}`;
2191
+ const verb = dryRun ? "would clean" : "cleaned";
2192
+ process.stdout.write(
2193
+ `${verb} ${label} [${result.status}]: ${result.removedPaths.length} path(s)
2194
+ `
2195
+ );
2196
+ printIndentedPaths(result.removedPaths, "");
2197
+ printIndentedPaths(result.failedPaths, "failed: ");
2198
+ }
2199
+ function printIndentedPaths(paths, prefix) {
2200
+ for (const path of paths) {
2201
+ process.stdout.write(` ${prefix}${path}
1794
2202
  `);
1795
- }
1796
2203
  }
1797
- return 0;
1798
2204
  }
1799
2205
  async function cmdStop(args) {
2206
+ const opts = parseStopOptions(args);
2207
+ const entry = lookupEntry(opts.target);
2208
+ if (!entry) {
2209
+ reportStopMissingTarget(opts);
2210
+ return 1;
2211
+ }
2212
+ return stopExistingEntry(entry, opts);
2213
+ }
2214
+ async function stopExistingEntry(entry, opts) {
2215
+ const status = validateStopEntry(entry);
2216
+ if (await handleInactiveStopEntry(entry, status, opts)) {
2217
+ return 0;
2218
+ }
2219
+ if (opts.dryRun) {
2220
+ reportStopDryRun(entry, opts);
2221
+ return 0;
2222
+ }
2223
+ return stopLiveEntry(entry, opts);
2224
+ }
2225
+ async function stopLiveEntry(entry, opts) {
2226
+ const sig = stopSignal(opts.force);
2227
+ if (!signalStopProcess(entry.pid, sig, opts, "STOP_KILL_FAILED")) {
2228
+ return 1;
2229
+ }
2230
+ await escalateIfNeeded(entry.pid, opts.force);
2231
+ await stopGvproxy(entry, sig, opts.force);
2232
+ finishStoppedEntry(entry, opts);
2233
+ return 0;
2234
+ }
2235
+ function parseStopOptions(args) {
1800
2236
  const { json, rest: afterJson } = consumeJsonFlag(args);
1801
2237
  const { dryRun, rest: afterDry } = consumeDryRunFlag(afterJson);
1802
- let force = false;
2238
+ const { force, rest } = consumeForceFlag(afterDry);
2239
+ return { json, dryRun, force, target: parseTargetFlags(rest, "stop") };
2240
+ }
2241
+ function consumeForceFlag(args) {
1803
2242
  const rest = [];
1804
- for (const a of afterDry) {
1805
- if (a === "--force" || a === "-9") {
2243
+ let force = false;
2244
+ for (const arg of args) {
2245
+ if (arg === "--force" || arg === "-9") {
1806
2246
  force = true;
1807
2247
  } else {
1808
- rest.push(a);
2248
+ rest.push(arg);
1809
2249
  }
1810
2250
  }
1811
- const target = parseTargetFlags(rest, "stop");
1812
- const entry = lookupEntry(target);
1813
- if (!entry) {
1814
- if (json) {
1815
- emitJsonError("VM_NOT_FOUND", `no running VM matched ${describeTarget(target)}`);
1816
- } else {
1817
- process.stderr.write(`machinen stop: no running VM matched ${describeTarget(target)}
2251
+ return { force, rest };
2252
+ }
2253
+ function reportStopMissingTarget(opts) {
2254
+ const message = `no running VM matched ${describeTarget(opts.target)}`;
2255
+ if (opts.json) {
2256
+ emitJsonError("VM_NOT_FOUND", message);
2257
+ } else {
2258
+ process.stderr.write(`machinen stop: ${message}
1818
2259
  `);
1819
- }
1820
- return 1;
1821
2260
  }
1822
- const emitStop = (status2) => {
1823
- if (json) {
1824
- emitJson({
1825
- schema_version: 1,
1826
- pid: entry.pid,
1827
- name: entry.name ?? null,
1828
- status: status2,
1829
- dry_run: dryRun
1830
- });
1831
- }
1832
- };
1833
- const status = validatePid(entry.pid, {
2261
+ }
2262
+ function emitStop(entry, opts, status) {
2263
+ if (!opts.json) {
2264
+ return;
2265
+ }
2266
+ emitJson({
2267
+ schema_version: 1,
2268
+ pid: entry.pid,
2269
+ name: entry.name ?? null,
2270
+ status,
2271
+ dry_run: opts.dryRun
2272
+ });
2273
+ }
2274
+ function validateStopEntry(entry) {
2275
+ return validatePid(entry.pid, {
1834
2276
  vmmExe: entry.vmmExe,
1835
2277
  startedAt: entry.startedAt
1836
2278
  });
2279
+ }
2280
+ async function handleInactiveStopEntry(entry, status, opts) {
1837
2281
  if (status === "recycled") {
1838
- if (!json) {
1839
- process.stderr.write(
1840
- `machinen stop: registry entry pid ${entry.pid} is now held by an unrelated process; ` + (dryRun ? "would skip kill and gc.\n" : "skipping kill and running gc.\n")
1841
- );
1842
- }
1843
- if (!dryRun) {
1844
- runGc({ pid: entry.pid });
1845
- }
1846
- emitStop("recycled");
1847
- return 0;
2282
+ reportRecycledStopEntry(entry, opts);
2283
+ gcStoppedEntry(entry, opts.dryRun);
2284
+ emitStop(entry, opts, "recycled");
2285
+ return true;
1848
2286
  }
1849
2287
  if (status === "dead") {
1850
- if (!json) {
1851
- process.stderr.write(
1852
- `machinen stop: pid ${entry.pid} already gone; ` + (dryRun ? "would gc.\n" : "running gc.\n")
1853
- );
1854
- }
1855
- if (!dryRun) {
1856
- runGc({ pid: entry.pid });
1857
- }
1858
- emitStop("already_dead");
1859
- return 0;
2288
+ reportDeadStopEntry(entry, opts);
2289
+ gcStoppedEntry(entry, opts.dryRun);
2290
+ emitStop(entry, opts, "already_dead");
2291
+ return true;
1860
2292
  }
1861
- if (dryRun) {
1862
- if (!json) {
1863
- const label = entry.name ? `${entry.name} (pid ${entry.pid})` : `pid ${entry.pid}`;
1864
- const sigLabel = force ? "SIGKILL" : "SIGTERM (escalates to SIGKILL after 2s)";
1865
- process.stdout.write(`would ${sigLabel} ${label}
2293
+ return false;
2294
+ }
2295
+ function reportRecycledStopEntry(entry, opts) {
2296
+ if (opts.json) {
2297
+ return;
2298
+ }
2299
+ process.stderr.write(
2300
+ `machinen stop: registry entry pid ${entry.pid} is now held by an unrelated process; ` + (opts.dryRun ? "would skip kill and gc.\n" : "skipping kill and running gc.\n")
2301
+ );
2302
+ }
2303
+ function reportDeadStopEntry(entry, opts) {
2304
+ if (opts.json) {
2305
+ return;
2306
+ }
2307
+ process.stderr.write(
2308
+ `machinen stop: pid ${entry.pid} already gone; ` + (opts.dryRun ? "would gc.\n" : "running gc.\n")
2309
+ );
2310
+ }
2311
+ function gcStoppedEntry(entry, dryRun) {
2312
+ if (!dryRun) {
2313
+ runGc({ pid: entry.pid });
2314
+ }
2315
+ }
2316
+ function reportStopDryRun(entry, opts) {
2317
+ if (!opts.json) {
2318
+ const sigLabel = opts.force ? "SIGKILL" : "SIGTERM (escalates to SIGKILL after 2s)";
2319
+ process.stdout.write(`would ${sigLabel} ${entryLabel(entry)}
1866
2320
  `);
1867
- }
1868
- emitStop("would_stop");
1869
- return 0;
1870
2321
  }
1871
- const sig = force ? "SIGKILL" : "SIGTERM";
2322
+ emitStop(entry, opts, "would_stop");
2323
+ }
2324
+ function stopSignal(force) {
2325
+ return force ? "SIGKILL" : "SIGTERM";
2326
+ }
2327
+ function signalStopProcess(pid, signal, opts, errorCode) {
1872
2328
  try {
1873
- process.kill(entry.pid, sig);
2329
+ process.kill(pid, signal);
2330
+ return true;
1874
2331
  } catch (err) {
1875
- const msg = `failed to signal pid ${entry.pid}: ${err instanceof Error ? err.message : String(err)}`;
1876
- if (json) {
1877
- emitJsonError("STOP_KILL_FAILED", msg);
1878
- } else {
1879
- process.stderr.write(`machinen stop: ${msg}
2332
+ reportStopSignalError(pid, err, opts, errorCode);
2333
+ return false;
2334
+ }
2335
+ }
2336
+ function reportStopSignalError(pid, err, opts, errorCode) {
2337
+ const msg = `failed to signal pid ${pid}: ${describeError(err)}`;
2338
+ if (opts.json) {
2339
+ emitJsonError(errorCode, msg);
2340
+ } else {
2341
+ process.stderr.write(`machinen stop: ${msg}
1880
2342
  `);
1881
- }
1882
- return 1;
1883
2343
  }
1884
- if (!force) {
1885
- await waitForExit(entry.pid, 2e3);
1886
- try {
1887
- process.kill(entry.pid, 0);
1888
- try {
1889
- process.kill(entry.pid, "SIGKILL");
1890
- } catch {
1891
- }
1892
- } catch {
1893
- }
2344
+ }
2345
+ async function escalateIfNeeded(pid, force) {
2346
+ if (force) {
2347
+ return;
1894
2348
  }
1895
- if (entry.gvproxyPid && entry.gvproxyExe) {
1896
- const gvStatus = validatePid(entry.gvproxyPid, { vmmExe: entry.gvproxyExe });
1897
- if (gvStatus === "alive") {
1898
- try {
1899
- process.kill(entry.gvproxyPid, sig);
1900
- } catch (err) {
1901
- process.stderr.write(
1902
- `machinen stop: failed to signal gvproxy pid ${entry.gvproxyPid}: ${err instanceof Error ? err.message : String(err)}
1903
- `
1904
- );
1905
- }
1906
- if (!force) {
1907
- await waitForExit(entry.gvproxyPid, 2e3);
1908
- try {
1909
- process.kill(entry.gvproxyPid, 0);
1910
- try {
1911
- process.kill(entry.gvproxyPid, "SIGKILL");
1912
- } catch {
1913
- }
1914
- } catch {
1915
- }
1916
- }
1917
- } else if (gvStatus === "recycled") {
1918
- process.stderr.write(
1919
- `machinen stop: gvproxy pid ${entry.gvproxyPid} now held by an unrelated process; skipping.
2349
+ await waitForExit(pid, 2e3);
2350
+ if (pidIsAlive(pid)) {
2351
+ tryKill(pid, "SIGKILL");
2352
+ }
2353
+ }
2354
+ function pidIsAlive(pid) {
2355
+ try {
2356
+ process.kill(pid, 0);
2357
+ return true;
2358
+ } catch {
2359
+ return false;
2360
+ }
2361
+ }
2362
+ function tryKill(pid, signal) {
2363
+ try {
2364
+ process.kill(pid, signal);
2365
+ } catch {
2366
+ }
2367
+ }
2368
+ async function stopGvproxy(entry, signal, force) {
2369
+ if (!entry.gvproxyPid || !entry.gvproxyExe) {
2370
+ return;
2371
+ }
2372
+ await handleGvproxyStatus(
2373
+ entry.gvproxyPid,
2374
+ validatePid(entry.gvproxyPid, { vmmExe: entry.gvproxyExe }),
2375
+ signal,
2376
+ force
2377
+ );
2378
+ }
2379
+ async function handleGvproxyStatus(pid, status, signal, force) {
2380
+ if (status === "alive") {
2381
+ await signalGvproxy(pid, signal, force);
2382
+ } else if (status === "recycled") {
2383
+ process.stderr.write(
2384
+ `machinen stop: gvproxy pid ${pid} now held by an unrelated process; skipping.
1920
2385
  `
1921
- );
1922
- }
2386
+ );
1923
2387
  }
2388
+ }
2389
+ async function signalGvproxy(pid, signal, force) {
2390
+ if (!signalStopProcess(pid, signal, { json: false }, "STOP_GVPROXY_KILL_FAILED")) {
2391
+ return;
2392
+ }
2393
+ await escalateIfNeeded(pid, force);
2394
+ }
2395
+ function finishStoppedEntry(entry, opts) {
1924
2396
  runGc({ pid: entry.pid });
1925
- if (json) {
1926
- emitStop("stopped");
2397
+ if (opts.json) {
2398
+ emitStop(entry, opts, "stopped");
1927
2399
  } else {
1928
- const label = entry.name ? `${entry.name} (pid ${entry.pid})` : `pid ${entry.pid}`;
1929
- process.stdout.write(`stopped ${label}
2400
+ process.stdout.write(`stopped ${entryLabel(entry)}
1930
2401
  `);
1931
2402
  }
1932
- return 0;
2403
+ }
2404
+ function entryLabel(entry) {
2405
+ return entry.name ? `${entry.name} (pid ${entry.pid})` : `pid ${entry.pid}`;
1933
2406
  }
1934
2407
  async function waitForExit(pid, timeoutMs) {
1935
2408
  const deadline = Date.now() + timeoutMs;
@@ -1943,406 +2416,510 @@ async function waitForExit(pid, timeoutMs) {
1943
2416
  }
1944
2417
  }
1945
2418
  function lookupEntry(target) {
1946
- for (const e of list()) {
1947
- if ("name" in target && e.name === target.name) {
1948
- return e;
1949
- }
1950
- if ("pid" in target && e.pid === target.pid) {
1951
- return e;
1952
- }
2419
+ return list().find((entry) => entryMatchesTarget(entry, target));
2420
+ }
2421
+ function entryMatchesTarget(entry, target) {
2422
+ if ("name" in target) {
2423
+ return entry.name === target.name;
1953
2424
  }
1954
- return void 0;
2425
+ return entry.pid === target.pid;
1955
2426
  }
1956
2427
  function describeTarget(target) {
1957
2428
  return "name" in target ? `name ${target.name}` : `pid ${target.pid}`;
1958
2429
  }
1959
2430
  async function cmdExec(args) {
1960
- let usePty = false;
1961
- const filtered = [];
1962
- for (const a of args) {
1963
- if (a === "--tty" || a === "--pty") {
1964
- usePty = true;
1965
- } else {
1966
- filtered.push(a);
1967
- }
2431
+ const parsed = parseExecArgs(args);
2432
+ const vm = await attach(parsed.target).catch(handleError);
2433
+ try {
2434
+ return await runExecCommand(vm, parsed);
2435
+ } finally {
2436
+ await vm.detach();
1968
2437
  }
2438
+ }
2439
+ function parseExecArgs(args) {
2440
+ const { usePty, filtered } = consumeExecPtyFlag(args);
1969
2441
  const dashIdx = filtered.indexOf("--");
1970
2442
  if (dashIdx === -1 || dashIdx === filtered.length - 1) {
1971
2443
  die("usage: machinen exec <name|pid> [--tty] -- <cmd>");
1972
2444
  }
1973
- const pre = filtered.slice(0, dashIdx);
1974
- const cmdArgs = filtered.slice(dashIdx + 1);
1975
- const target = parseTargetFlags(pre, "exec");
1976
- const vm = await attach(target).catch(handleError);
1977
- try {
1978
- const joined = cmdArgs.join(" ");
1979
- if (usePty) {
1980
- if (!process.stdin.isTTY) {
1981
- die("machinen exec --tty: stdin is not a TTY; pass via terminal or drop --tty");
1982
- }
1983
- return await runPtyExec(vm, joined);
2445
+ return {
2446
+ usePty,
2447
+ target: parseTargetFlags(filtered.slice(0, dashIdx), "exec"),
2448
+ cmd: filtered.slice(dashIdx + 1).join(" ")
2449
+ };
2450
+ }
2451
+ function consumeExecPtyFlag(args) {
2452
+ const filtered = [];
2453
+ let usePty = false;
2454
+ for (const arg of args) {
2455
+ if (arg === "--tty" || arg === "--pty") {
2456
+ usePty = true;
2457
+ } else {
2458
+ filtered.push(arg);
1984
2459
  }
1985
- const res = await vm.execRaw(joined, {
1986
- onStdout: (chunk) => process.stdout.write(chunk),
1987
- onStderr: (chunk) => process.stderr.write(chunk)
1988
- });
1989
- return res.exitCode;
1990
- } finally {
1991
- await vm.detach();
1992
2460
  }
2461
+ return { usePty, filtered };
2462
+ }
2463
+ async function runExecCommand(vm, parsed) {
2464
+ if (parsed.usePty) {
2465
+ assertExecPtyTty();
2466
+ return runPtyExec(vm, parsed.cmd);
2467
+ }
2468
+ return runRawExec(vm, parsed.cmd);
2469
+ }
2470
+ function assertExecPtyTty() {
2471
+ if (!process.stdin.isTTY) {
2472
+ die("machinen exec --tty: stdin is not a TTY; pass via terminal or drop --tty");
2473
+ }
2474
+ }
2475
+ async function runRawExec(vm, cmd) {
2476
+ const res = await vm.execRaw(cmd, {
2477
+ onStdout: (chunk) => process.stdout.write(chunk),
2478
+ onStderr: (chunk) => process.stderr.write(chunk)
2479
+ });
2480
+ return res.exitCode;
1993
2481
  }
1994
2482
  async function runPtyExec(vm, cmd) {
1995
- const stdin = process.stdin;
1996
- const stdout = process.stdout;
1997
- const initialCols = stdout.columns ?? 80;
1998
- const initialRows = stdout.rows ?? 24;
1999
- const wasRaw = stdin.isRaw === true;
2000
- stdin.setRawMode(true);
2001
- stdin.resume();
2483
+ const tty = enterPtyRawMode();
2002
2484
  const handle = vm.execPty(cmd, {
2003
- cols: initialCols,
2004
- rows: initialRows,
2005
- stdin,
2006
- stdout
2485
+ cols: tty.cols,
2486
+ rows: tty.rows,
2487
+ stdin: process.stdin,
2488
+ stdout: process.stdout
2007
2489
  });
2008
- const onResize = () => {
2009
- handle.resize(stdout.columns ?? initialCols, stdout.rows ?? initialRows);
2010
- };
2011
- stdout.on("resize", onResize);
2490
+ const onResize = () => handle.resize(process.stdout.columns ?? tty.cols, process.stdout.rows ?? tty.rows);
2491
+ process.stdout.on("resize", onResize);
2012
2492
  try {
2013
2493
  const { exitCode } = await handle.result;
2014
2494
  return exitCode;
2015
2495
  } finally {
2016
- stdout.removeListener("resize", onResize);
2017
- if (!wasRaw) {
2018
- try {
2019
- stdin.setRawMode(false);
2020
- } catch {
2021
- }
2022
- }
2496
+ process.stdout.removeListener("resize", onResize);
2497
+ tty.restore();
2498
+ }
2499
+ }
2500
+ function enterPtyRawMode() {
2501
+ const wasRaw = process.stdin.isRaw === true;
2502
+ process.stdin.setRawMode(true);
2503
+ process.stdin.resume();
2504
+ return {
2505
+ cols: process.stdout.columns ?? 80,
2506
+ rows: process.stdout.rows ?? 24,
2507
+ restore: () => restorePtyRawMode(wasRaw)
2508
+ };
2509
+ }
2510
+ function restorePtyRawMode(wasRaw) {
2511
+ if (wasRaw) {
2512
+ return;
2513
+ }
2514
+ try {
2515
+ process.stdin.setRawMode(false);
2516
+ } catch {
2023
2517
  }
2024
2518
  }
2025
2519
  async function cmdSnapshot(args) {
2520
+ const opts = parseSnapshotOptions(args);
2521
+ if (opts.dryRun) {
2522
+ return snapshotDryRun(opts);
2523
+ }
2524
+ return runSnapshot(opts);
2525
+ }
2526
+ function parseSnapshotOptions(args) {
2026
2527
  const { json, rest: afterJson } = consumeJsonFlag(args);
2027
2528
  const { dryRun, rest: afterDry } = consumeDryRunFlag(afterJson);
2529
+ const { keepAlive, rest } = consumeKeepAliveFlag(afterDry);
2530
+ const { target, rest: afterTarget } = resolveTarget(rest, "snapshot");
2531
+ const outDir = parseSnapshotOutDir(afterTarget);
2532
+ return { json, dryRun, keepAlive, target, outDir, resolvedOutDir: resolve(outDir) };
2533
+ }
2534
+ function consumeKeepAliveFlag(args) {
2535
+ const rest = [];
2028
2536
  let keepAlive = false;
2029
- const remaining = [];
2030
- for (const a of afterDry) {
2031
- if (a === "--keep-alive") {
2537
+ for (const arg of args) {
2538
+ if (arg === "--keep-alive") {
2032
2539
  keepAlive = true;
2033
2540
  } else {
2034
- remaining.push(a);
2541
+ rest.push(arg);
2035
2542
  }
2036
2543
  }
2037
- const { target, rest: afterTarget } = resolveTarget(remaining, "snapshot");
2038
- if (afterTarget.length === 0) {
2544
+ return { keepAlive, rest };
2545
+ }
2546
+ function parseSnapshotOutDir(args) {
2547
+ if (args.length === 0) {
2039
2548
  die("usage: machinen snapshot <name|pid> <out-dir> [--keep-alive] [--dry-run] [--json]");
2040
2549
  }
2041
- if (afterTarget.length > 1) {
2042
- die(`unknown argument: ${afterTarget[1]}`);
2550
+ if (args.length > 1) {
2551
+ die(`unknown argument: ${args[1]}`);
2552
+ }
2553
+ return args[0];
2554
+ }
2555
+ function snapshotDryRun(opts) {
2556
+ const entry = lookupEntry(opts.target);
2557
+ if (!entry) {
2558
+ reportSnapshotMissingTarget(opts);
2559
+ return 1;
2560
+ }
2561
+ reportSnapshotDryRun(entry, opts);
2562
+ return 0;
2563
+ }
2564
+ function reportSnapshotMissingTarget(opts) {
2565
+ const msg = `no running VM matched ${describeTarget(opts.target)}`;
2566
+ if (opts.json) {
2567
+ emitJsonError("VM_NOT_FOUND", msg);
2568
+ } else {
2569
+ process.stderr.write(`machinen snapshot: ${msg}
2570
+ `);
2571
+ }
2572
+ }
2573
+ function reportSnapshotDryRun(entry, opts) {
2574
+ if (opts.json) {
2575
+ emitSnapshotJson(opts.resolvedOutDir, 0, true);
2576
+ return;
2577
+ }
2578
+ const suffix = opts.keepAlive ? " (--keep-alive)\n" : "\n";
2579
+ process.stdout.write(`would snapshot ${entryLabel(entry)} \u2192 ${opts.resolvedOutDir}${suffix}`);
2580
+ }
2581
+ async function runSnapshot(opts) {
2582
+ const vm = await attach(opts.target).catch(handleError);
2583
+ const quiet = createSnapshotQuietState(vm, opts);
2584
+ try {
2585
+ const res = await vm.snapshot({
2586
+ outDir: opts.resolvedOutDir,
2587
+ leaveRunning: opts.keepAlive,
2588
+ tcpClose: opts.keepAlive,
2589
+ onLog: snapshotOnLog(quiet)
2590
+ });
2591
+ reportSnapshotSuccess(res.snapDir, res.elapsedMs, opts);
2592
+ return 0;
2593
+ } catch (err) {
2594
+ handleSnapshotFailure(err, quiet);
2595
+ } finally {
2596
+ await vm.detach();
2597
+ }
2598
+ }
2599
+ function createSnapshotQuietState(vm, opts) {
2600
+ const headlineName = vm.name ?? `pid ${vm.pid}`;
2601
+ const showHeadlines = isQuiet() && !opts.json;
2602
+ const buffer = new RingBuffer();
2603
+ if (showHeadlines) {
2604
+ printHeadline(`snapshotting ${headlineName}\u2026`);
2043
2605
  }
2044
- const outDir = afterTarget[0];
2045
- const resolvedOutDir = resolve(outDir);
2046
- if (dryRun) {
2047
- const entry = lookupEntry(target);
2048
- if (!entry) {
2049
- const msg = `no running VM matched ${describeTarget(target)}`;
2050
- if (json) {
2051
- emitJsonError("VM_NOT_FOUND", msg);
2052
- } else {
2053
- process.stderr.write(`machinen snapshot: ${msg}
2054
- `);
2055
- }
2056
- return 1;
2606
+ return { headlineName, showHeadlines, buffer, filter: null, filterOut: null };
2607
+ }
2608
+ function snapshotOnLog(quiet) {
2609
+ return (evt) => {
2610
+ if (evt.source === "phase") {
2611
+ return;
2057
2612
  }
2058
- if (json) {
2059
- emitJson({
2060
- schema_version: 1,
2061
- snap_dir: resolvedOutDir,
2062
- elapsed_ms: 0,
2063
- dry_run: true
2064
- });
2613
+ if (quiet.showHeadlines) {
2614
+ quiet.buffer.push(evt.chunk);
2065
2615
  } else {
2066
- const label = entry.name ? `${entry.name} (pid ${entry.pid})` : `pid ${entry.pid}`;
2067
- process.stdout.write(
2068
- `would snapshot ${label} \u2192 ${resolvedOutDir}` + (keepAlive ? " (--keep-alive)\n" : "\n")
2069
- );
2616
+ process.stderr.write(evt.chunk);
2070
2617
  }
2071
- return 0;
2618
+ };
2619
+ }
2620
+ function reportSnapshotSuccess(snapDir, elapsedMs, opts) {
2621
+ if (opts.json) {
2622
+ emitSnapshotJson(snapDir, elapsedMs, false);
2623
+ return;
2072
2624
  }
2073
- const vm = await attach(target).catch(handleError);
2074
- const snapHeadlineName = vm.name ?? `pid ${vm.pid}`;
2075
- const showHeadlines = isQuiet() && !json;
2076
- const buffer = new RingBuffer();
2077
- if (showHeadlines) {
2078
- printHeadline(`snapshotting ${snapHeadlineName}\u2026`);
2625
+ process.stdout.write(`snapshot: ${snapDir} (${elapsedMs}ms)
2626
+ `);
2627
+ }
2628
+ function emitSnapshotJson(snapDir, elapsedMs, dryRun) {
2629
+ emitJson({ schema_version: 1, snap_dir: snapDir, elapsed_ms: elapsedMs, dry_run: dryRun });
2630
+ }
2631
+ function handleSnapshotFailure(err, quiet) {
2632
+ if (quiet.showHeadlines) {
2633
+ failQuiet(`snapshot ${quiet.headlineName} failed: ${describeError(err)}`, {
2634
+ buffer: quiet.buffer
2635
+ });
2079
2636
  }
2637
+ handleError(err);
2638
+ }
2639
+ async function cmdFork(args) {
2640
+ const opts = await prepareForkCommand(args);
2641
+ const vm = await attach(opts.target).catch(handleError);
2080
2642
  try {
2081
- const res = await vm.snapshot({
2082
- outDir: resolvedOutDir,
2083
- leaveRunning: keepAlive,
2084
- tcpClose: keepAlive,
2085
- onLog: (evt) => {
2086
- if (evt.source === "phase") {
2087
- return;
2088
- }
2089
- if (showHeadlines) {
2090
- buffer.push(evt.chunk);
2091
- } else {
2092
- process.stderr.write(evt.chunk);
2093
- }
2094
- }
2095
- });
2096
- if (json) {
2097
- emitJson({
2098
- schema_version: 1,
2099
- snap_dir: res.snapDir,
2100
- elapsed_ms: res.elapsedMs,
2101
- dry_run: false
2102
- });
2103
- } else {
2104
- process.stdout.write(`snapshot: ${res.snapDir} (${res.elapsedMs}ms)
2105
- `);
2643
+ const fork = await startForkVm(vm, opts);
2644
+ reportForkStarted(fork, opts);
2645
+ if (opts.parsed.detach) {
2646
+ return detachFork(fork, opts);
2106
2647
  }
2107
- return 0;
2648
+ return runForkAttachedSession(fork, opts.quiet);
2108
2649
  } catch (err) {
2109
- if (showHeadlines) {
2110
- failQuiet(`snapshot ${snapHeadlineName} failed: ${describeError(err)}`, {
2111
- buffer
2112
- });
2113
- }
2114
2650
  handleError(err);
2115
2651
  } finally {
2116
2652
  await vm.detach();
2117
2653
  }
2118
2654
  }
2119
- async function cmdFork(args) {
2120
- const { json, rest: forkArgs } = consumeJsonFlag(args);
2121
- let parsed;
2655
+ async function prepareForkCommand(args) {
2656
+ const { json, rest } = consumeJsonFlag(args);
2657
+ const parsed = parseForkCommandArgs(rest);
2658
+ const target = parseTargetFlags(parsed.rest, "fork");
2659
+ validateForkCommand(json, parsed);
2660
+ const paths = await resolveCliBaseAssets();
2661
+ const resolvedOutDir = resolveForkOutDir(parsed.outDir);
2662
+ return {
2663
+ json,
2664
+ target,
2665
+ parsed,
2666
+ paths,
2667
+ resolvedOutDir,
2668
+ quiet: createForkQuietState(json, parsed, target)
2669
+ };
2670
+ }
2671
+ function parseForkCommandArgs(args) {
2122
2672
  try {
2123
- parsed = parseForkArgs(forkArgs);
2673
+ return parseForkArgs(args);
2124
2674
  } catch (err) {
2125
2675
  handleError(err);
2126
2676
  }
2127
- const {
2128
- newName,
2129
- outDir,
2130
- tcpKeep,
2131
- detach,
2132
- lazy,
2133
- portForward,
2134
- mount,
2135
- liveMounts,
2136
- env,
2137
- guestCwd,
2138
- memory,
2139
- rest
2140
- } = parsed;
2141
- const target = parseTargetFlags(rest, "fork");
2142
- if (json && !detach) {
2677
+ }
2678
+ function validateForkCommand(json, parsed) {
2679
+ if (json && !parsed.detach) {
2143
2680
  die("fork --json is only meaningful with --detach (attached forks take over stdio).");
2144
2681
  }
2145
- const assetsOverride = process.env.MACHINEN_ASSETS_DIR;
2146
- if (assetsOverride) {
2147
- validateAssetsDir(assetsOverride);
2148
- } else if (!baseAssetsComplete(RELEASE_TAG)) {
2149
- process.stderr.write(`machinen: fetching base assets for ${RELEASE_TAG} (first run)
2150
- `);
2151
- await ensureBaseAssets(RELEASE_TAG);
2682
+ }
2683
+ function resolveForkOutDir(outDir) {
2684
+ if (outDir) {
2685
+ return resolve(outDir);
2152
2686
  }
2153
- const baseDir = assetsOverride ? resolve(assetsOverride) : baseDirFor(RELEASE_TAG);
2154
- const kernelPath = join2(baseDir, assetsOverride ? "Image-arm64" : "Image");
2155
- const dtbPath = join2(baseDir, assetsOverride ? "virt-arm64.dtb" : "virt.dtb");
2156
- const imagePath = join2(baseDir, assetsOverride ? "rootfs-debian-arm64.tar.gz" : "rootfs.tar.gz");
2157
- const resolvedOutDir = outDir ? resolve(outDir) : mkdtempSync(join2(tmpdir(), "machinen-fork-"));
2158
- const vm = await attach(target).catch(handleError);
2159
- const sourceLabel = "name" in target ? target.name : `pid ${target.pid}`;
2160
- const forkHeadlineName = newName ?? sourceLabel;
2161
- const showHeadlines = isQuiet() && !(detach && json);
2162
- const forkT0 = Date.now();
2687
+ return mkdtempSync(join2(tmpdir(), "machinen-fork-"));
2688
+ }
2689
+ function createForkQuietState(json, parsed, target) {
2690
+ const sourceLabel = describeForkSource(target);
2691
+ const headlineName = parsed.newName ?? sourceLabel;
2163
2692
  const buffer = new RingBuffer();
2164
- let filter = null;
2165
- if (showHeadlines) {
2166
- printHeadline(`forking ${sourceLabel} \u2192 ${forkHeadlineName}\u2026`);
2167
- if (!detach) {
2168
- filter = new NoiseFilter({
2169
- buffer,
2170
- out: process.stderr,
2171
- onReady: () => {
2172
- printHeadline(`fork ready in ${formatElapsed(Date.now() - forkT0)}`);
2173
- }
2174
- });
2175
- }
2693
+ if (!shouldShowForkHeadlines(json, parsed)) {
2694
+ return forkOperatorQuietState(headlineName, buffer);
2176
2695
  }
2177
- const onLog = filter ? (evt) => {
2178
- if (evt.source === "guest-console") {
2179
- filter.push(evt.chunk);
2180
- }
2181
- } : showHeadlines ? (evt) => {
2182
- if (evt.source === "guest-console") {
2183
- buffer.push(evt.chunk);
2184
- }
2185
- } : (evt) => {
2186
- if (evt.source !== "phase") {
2187
- process.stderr.write(evt.chunk);
2696
+ return createVisibleForkQuietState(parsed, sourceLabel, headlineName, buffer);
2697
+ }
2698
+ function shouldShowForkHeadlines(json, parsed) {
2699
+ if (!isQuiet()) {
2700
+ return false;
2701
+ }
2702
+ if (parsed.detach && json) {
2703
+ return false;
2704
+ }
2705
+ return true;
2706
+ }
2707
+ function createVisibleForkQuietState(parsed, sourceLabel, headlineName, buffer) {
2708
+ printHeadline(`forking ${sourceLabel} \u2192 ${headlineName}\u2026`);
2709
+ if (parsed.detach) {
2710
+ return bootBufferOnlyQuietState(headlineName, true, buffer);
2711
+ }
2712
+ return forkFilteredQuietState(headlineName, true, buffer, Date.now());
2713
+ }
2714
+ function describeForkSource(target) {
2715
+ return "name" in target ? target.name : `pid ${target.pid}`;
2716
+ }
2717
+ function forkOperatorQuietState(headlineName, buffer) {
2718
+ return {
2719
+ headlineName,
2720
+ showHeadlines: false,
2721
+ buffer,
2722
+ filter: null,
2723
+ filterOut: null,
2724
+ onLog: operatorForkOnLog
2725
+ };
2726
+ }
2727
+ function operatorForkOnLog(evt) {
2728
+ if (evt.source !== "phase") {
2729
+ process.stderr.write(evt.chunk);
2730
+ }
2731
+ }
2732
+ function forkFilteredQuietState(headlineName, showHeadlines, buffer, forkT0) {
2733
+ const filter = new NoiseFilter({
2734
+ buffer,
2735
+ out: process.stderr,
2736
+ onReady: () => {
2737
+ printHeadline(`fork ready in ${formatElapsed(Date.now() - forkT0)}`);
2188
2738
  }
2739
+ });
2740
+ return {
2741
+ headlineName,
2742
+ showHeadlines,
2743
+ buffer,
2744
+ filter,
2745
+ filterOut: null,
2746
+ onLog: guestConsoleOnLog((chunk) => filter.push(chunk))
2189
2747
  };
2748
+ }
2749
+ async function startForkVm(vm, opts) {
2190
2750
  try {
2191
- let fork;
2192
- try {
2193
- fork = await vm.fork({
2194
- name: newName,
2195
- outDir: resolvedOutDir,
2196
- image: imagePath,
2197
- kernel: kernelPath,
2198
- dtb: dtbPath,
2199
- tcpKeep,
2200
- lazy,
2201
- portForward: portForward.length > 0 ? portForward : void 0,
2202
- mount,
2203
- liveMounts,
2204
- env,
2205
- guestCwd,
2206
- memory,
2207
- onLog
2208
- });
2209
- } catch (err) {
2210
- filter?.flush();
2211
- if (showHeadlines) {
2212
- failQuiet(`fork ${forkHeadlineName} failed: ${describeError(err)}`, {
2213
- buffer
2214
- });
2215
- }
2216
- throw err;
2217
- }
2218
- if (!showHeadlines && !json) {
2219
- process.stderr.write(`forked: ${fork.name ?? "<anonymous>"} (pid ${fork.pid})
2751
+ return await vm.fork({
2752
+ name: opts.parsed.newName,
2753
+ outDir: opts.resolvedOutDir,
2754
+ image: opts.paths.defaultImagePath,
2755
+ kernel: opts.paths.kernelPath,
2756
+ dtb: opts.paths.dtbPath,
2757
+ tcpKeep: opts.parsed.tcpKeep,
2758
+ lazy: opts.parsed.lazy,
2759
+ portForward: optionalList(opts.parsed.portForward),
2760
+ mount: opts.parsed.mount,
2761
+ liveMounts: opts.parsed.liveMounts,
2762
+ env: opts.parsed.env,
2763
+ guestCwd: opts.parsed.guestCwd,
2764
+ memory: opts.parsed.memory,
2765
+ onLog: opts.quiet.onLog
2766
+ });
2767
+ } catch (err) {
2768
+ handleForkFailure(err, opts.quiet);
2769
+ }
2770
+ }
2771
+ function handleForkFailure(err, quiet) {
2772
+ quiet.filter?.flush();
2773
+ if (quiet.showHeadlines) {
2774
+ failQuiet(`fork ${quiet.headlineName} failed: ${describeError(err)}`, {
2775
+ buffer: quiet.buffer
2776
+ });
2777
+ }
2778
+ handleError(err);
2779
+ }
2780
+ function reportForkStarted(fork, opts) {
2781
+ if (!shouldPrintForkStarted(opts)) {
2782
+ return;
2783
+ }
2784
+ process.stderr.write(`forked: ${fork.name ?? "<anonymous>"} (pid ${fork.pid})
2220
2785
  `);
2221
- if (!outDir) {
2222
- process.stderr.write(`bundle: ${resolvedOutDir} (rm -rf when the fork exits)
2786
+ printForkBundleHint(opts);
2787
+ }
2788
+ function shouldPrintForkStarted(opts) {
2789
+ if (opts.quiet.showHeadlines) {
2790
+ return false;
2791
+ }
2792
+ return !opts.json;
2793
+ }
2794
+ function printForkBundleHint(opts) {
2795
+ if (!opts.parsed.outDir) {
2796
+ process.stderr.write(`bundle: ${opts.resolvedOutDir} (rm -rf when the fork exits)
2223
2797
  `);
2224
- }
2225
- }
2226
- if (detach) {
2227
- await fork.detach();
2228
- if (json) {
2229
- emitJson({
2230
- schema_version: 1,
2231
- pid: fork.pid,
2232
- name: fork.name ?? null,
2233
- source: "name" in target ? target.name : `pid ${target.pid}`,
2234
- bundle_dir: resolvedOutDir,
2235
- ephemeral: !outDir
2236
- });
2237
- }
2238
- return 0;
2239
- }
2240
- fork.stdout.pipe(process.stdout);
2241
- if (!filter) {
2242
- fork.stderr.pipe(process.stderr);
2243
- }
2244
- const restoreStdin = rawModeStdinIfTTY();
2245
- const cancelHintRepeat = printCtrlDHint();
2246
- const promptNudge = setTimeout(() => {
2247
- try {
2248
- fork.stdin.write("\r");
2249
- } catch {
2250
- }
2251
- }, 1500);
2252
- promptNudge.unref();
2253
- let forwardedSignal = null;
2254
- const onSigint = () => {
2255
- forwardedSignal = "SIGINT";
2256
- void fork.kill();
2257
- };
2258
- const onSigterm = () => {
2259
- forwardedSignal = "SIGTERM";
2260
- void fork.kill();
2261
- };
2262
- process.on("SIGINT", onSigint);
2263
- process.on("SIGTERM", onSigterm);
2264
- pipeStdinToVm(fork.stdin, () => {
2265
- process.stderr.write("\nmachinen: Ctrl-D \u2014 stopping VM\n");
2266
- forwardedSignal = "SIGTERM";
2267
- void fork.kill();
2798
+ }
2799
+ }
2800
+ async function detachFork(fork, opts) {
2801
+ await fork.detach();
2802
+ if (opts.json) {
2803
+ emitJson({
2804
+ schema_version: 1,
2805
+ pid: fork.pid,
2806
+ name: fork.name ?? null,
2807
+ source: describeForkSource(opts.target),
2808
+ bundle_dir: opts.resolvedOutDir,
2809
+ ephemeral: !opts.parsed.outDir
2810
+ });
2811
+ }
2812
+ return 0;
2813
+ }
2814
+ async function runForkAttachedSession(fork, quiet) {
2815
+ const cancelPromptNudge = scheduleForkPromptNudge(fork);
2816
+ try {
2817
+ return await runAttachedVmSession(fork, {
2818
+ filter: quiet.filter,
2819
+ buffer: quiet.buffer,
2820
+ preReadyExitSummary: (code) => `fork ${quiet.headlineName} exited ${code} before reaching ready`
2268
2821
  });
2269
- try {
2270
- const { code } = await fork.wait();
2271
- filter?.flush();
2272
- if (forwardedSignal === "SIGINT") {
2273
- return 130;
2274
- }
2275
- if (forwardedSignal === "SIGTERM") {
2276
- return 143;
2277
- }
2278
- if (filter && !filter.ready && code != null && code !== 0 && !forwardedSignal) {
2279
- printDiagnostics(`fork ${forkHeadlineName} exited ${code} before reaching ready`, {
2280
- buffer
2281
- });
2282
- }
2283
- return code ?? 0;
2284
- } finally {
2285
- process.off("SIGINT", onSigint);
2286
- process.off("SIGTERM", onSigterm);
2287
- cancelHintRepeat();
2288
- restoreStdin();
2289
- }
2290
- } catch (err) {
2291
- handleError(err);
2292
2822
  } finally {
2293
- await vm.detach();
2823
+ cancelPromptNudge();
2824
+ }
2825
+ }
2826
+ function scheduleForkPromptNudge(fork) {
2827
+ const promptNudge = setTimeout(() => tryWriteForkPromptNudge(fork), 1500);
2828
+ promptNudge.unref();
2829
+ return () => clearTimeout(promptNudge);
2830
+ }
2831
+ function tryWriteForkPromptNudge(fork) {
2832
+ try {
2833
+ fork.stdin.write("\r");
2834
+ } catch {
2294
2835
  }
2295
2836
  }
2296
2837
  async function cmdAttach(args) {
2297
- let shell = "/bin/bash -i";
2298
- let tail;
2299
- const filtered = [];
2838
+ const opts = parseAttachOptions(args);
2839
+ printAttachTailIfRequested(opts);
2840
+ const vm = await attach(opts.target).catch(handleError);
2841
+ return runAttachedPty(vm, opts.shell);
2842
+ }
2843
+ function parseAttachOptions(args) {
2844
+ const state = {
2845
+ shell: "/bin/bash -i",
2846
+ tail: void 0,
2847
+ rest: []
2848
+ };
2300
2849
  for (let i = 0; i < args.length; i++) {
2301
- const a = args[i];
2302
- if (a === "--shell" || a.startsWith("--shell=")) {
2303
- const v = a === "--shell" ? args[++i] : a.slice("--shell=".length);
2304
- if (!v) {
2305
- die("--shell requires a value");
2306
- }
2307
- shell = v;
2308
- } else if (a === "--tail" || a.startsWith("--tail=")) {
2309
- let v;
2310
- if (a === "--tail") {
2311
- const peek = args[i + 1];
2312
- if (peek && /^[0-9]+$/.test(peek)) {
2313
- v = peek;
2314
- i++;
2315
- }
2316
- } else {
2317
- v = a.slice("--tail=".length);
2318
- }
2319
- if (v === void 0) {
2320
- tail = "all";
2321
- } else {
2322
- const n = Number(v);
2323
- if (!Number.isInteger(n) || n < 0) {
2324
- die(`--tail: expected a non-negative integer, got '${v}'`);
2325
- }
2326
- tail = n;
2327
- }
2328
- } else {
2329
- filtered.push(a);
2330
- }
2850
+ i = consumeAttachArg(args, i, state);
2331
2851
  }
2332
- const target = parseTargetFlags(filtered, "attach");
2333
- if (tail !== void 0) {
2334
- const entry = lookupEntry(target);
2335
- if (!entry) {
2336
- die(`machinen attach: no running VM matched ${describeTarget(target)}`);
2337
- }
2338
- if (!entry.bootLogPath) {
2339
- die(
2340
- `machinen attach --tail: VM was not booted with --detached, no snapshot exists. Use 'machinen attach' (no --tail) for live console access.`
2341
- );
2342
- }
2343
- printBootLogTail(entry.bootLogPath, tail);
2852
+ return { shell: state.shell, tail: state.tail, target: parseTargetFlags(state.rest, "attach") };
2853
+ }
2854
+ function consumeAttachArg(args, index, state) {
2855
+ const arg = args[index];
2856
+ const handler = attachArgHandler(arg);
2857
+ if (handler) {
2858
+ return handler(args, index, arg, state);
2344
2859
  }
2345
- const vm = await attach(target).catch(handleError);
2860
+ state.rest.push(arg);
2861
+ return index;
2862
+ }
2863
+ var ATTACH_ARG_HANDLERS = [
2864
+ [(arg) => arg === "--shell" || arg.startsWith("--shell="), consumeAttachShell],
2865
+ [(arg) => arg === "--tail" || arg.startsWith("--tail="), consumeAttachTail]
2866
+ ];
2867
+ function attachArgHandler(arg) {
2868
+ return ATTACH_ARG_HANDLERS.find(([matches]) => matches(arg))?.[1];
2869
+ }
2870
+ function consumeAttachShell(args, index, arg, state) {
2871
+ const value = arg === "--shell" ? args[index + 1] : arg.slice("--shell=".length);
2872
+ if (!value) {
2873
+ die("--shell requires a value");
2874
+ }
2875
+ state.shell = value;
2876
+ return arg === "--shell" ? index + 1 : index;
2877
+ }
2878
+ function consumeAttachTail(args, index, arg, state) {
2879
+ const { value, nextIndex } = attachTailValue(args, index, arg);
2880
+ state.tail = parseAttachTail(value);
2881
+ return nextIndex;
2882
+ }
2883
+ function attachTailValue(args, index, arg) {
2884
+ if (arg !== "--tail") {
2885
+ return { value: arg.slice("--tail=".length), nextIndex: index };
2886
+ }
2887
+ const peek = args[index + 1];
2888
+ if (peek && /^[0-9]+$/.test(peek)) {
2889
+ return { value: peek, nextIndex: index + 1 };
2890
+ }
2891
+ return { value: void 0, nextIndex: index };
2892
+ }
2893
+ function parseAttachTail(value) {
2894
+ if (value === void 0) {
2895
+ return "all";
2896
+ }
2897
+ const n = Number(value);
2898
+ if (!Number.isInteger(n) || n < 0) {
2899
+ die(`--tail: expected a non-negative integer, got '${value}'`);
2900
+ }
2901
+ return n;
2902
+ }
2903
+ function printAttachTailIfRequested(opts) {
2904
+ if (opts.tail === void 0) {
2905
+ return;
2906
+ }
2907
+ const entry = lookupAttachTailEntry(opts.target);
2908
+ printBootLogTail(entry.bootLogPath, opts.tail);
2909
+ }
2910
+ function lookupAttachTailEntry(target) {
2911
+ const entry = lookupEntry(target);
2912
+ if (!entry) {
2913
+ die(`machinen attach: no running VM matched ${describeTarget(target)}`);
2914
+ }
2915
+ if (!entry.bootLogPath) {
2916
+ die(
2917
+ `machinen attach --tail: VM was not booted with --detached, no snapshot exists. Use 'machinen attach' (no --tail) for live console access.`
2918
+ );
2919
+ }
2920
+ return entry;
2921
+ }
2922
+ async function runAttachedPty(vm, shell) {
2346
2923
  if (!process.stdin.isTTY) {
2347
2924
  await vm.detach();
2348
2925
  die("machinen attach: stdin is not a TTY (pipe scripts via `machinen repl` instead)");
@@ -2371,6 +2948,15 @@ function printBootLogTail(path, tail) {
2371
2948
  async function cmdRepl(args) {
2372
2949
  const target = parseTargetFlags(args, "repl");
2373
2950
  const vm = await attach(target).catch(handleError);
2951
+ printReplIntro(vm);
2952
+ try {
2953
+ await runReplLoop(vm);
2954
+ return 0;
2955
+ } finally {
2956
+ await vm.detach();
2957
+ }
2958
+ }
2959
+ function printReplIntro(vm) {
2374
2960
  process.stderr.write(`repl: ${vm.name ?? `pid ${vm.pid}`}
2375
2961
  `);
2376
2962
  process.stderr.write(
@@ -2380,22 +2966,22 @@ for an interactive shell with job control + TUI support, use:
2380
2966
  Ctrl-D to exit.
2381
2967
  `
2382
2968
  );
2383
- try {
2384
- const { createInterface } = await import("readline");
2385
- const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false });
2386
- for await (const line of rl) {
2387
- if (line.length === 0) {
2388
- continue;
2389
- }
2390
- await vm.execRaw(line, {
2391
- onStdout: (chunk) => process.stdout.write(chunk),
2392
- onStderr: (chunk) => process.stderr.write(chunk)
2393
- });
2394
- }
2395
- return 0;
2396
- } finally {
2397
- await vm.detach();
2969
+ }
2970
+ async function runReplLoop(vm) {
2971
+ const { createInterface } = await import("readline");
2972
+ const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false });
2973
+ for await (const line of rl) {
2974
+ await runReplLine(vm, line);
2975
+ }
2976
+ }
2977
+ async function runReplLine(vm, line) {
2978
+ if (line.length === 0) {
2979
+ return;
2398
2980
  }
2981
+ await vm.execRaw(line, {
2982
+ onStdout: (chunk) => process.stdout.write(chunk),
2983
+ onStderr: (chunk) => process.stderr.write(chunk)
2984
+ });
2399
2985
  }
2400
2986
  async function cmdAgentContext(args) {
2401
2987
  for (const a of args) {
@@ -2407,86 +2993,103 @@ async function cmdAgentContext(args) {
2407
2993
  return 0;
2408
2994
  }
2409
2995
  async function cmdFeedback(args) {
2410
- const { json, rest: afterJson } = consumeJsonFlag(args);
2411
- let listMode = false;
2412
- const positional = [];
2413
- for (const a of afterJson) {
2414
- if (a === "--list") {
2415
- listMode = true;
2416
- } else if (a.startsWith("--")) {
2417
- die(`unknown argument: ${a}`);
2418
- } else {
2419
- positional.push(a);
2420
- }
2996
+ const opts = parseFeedbackOptions(args);
2997
+ if (opts.listMode) {
2998
+ return listFeedback(opts);
2421
2999
  }
2422
- if (listMode) {
2423
- if (positional.length > 0) {
2424
- die("machinen feedback --list takes no positional arguments");
2425
- }
2426
- const entries = readFeedback();
2427
- if (json) {
2428
- emitJson({ schema_version: 1, entries });
2429
- return 0;
2430
- }
2431
- if (entries.length === 0) {
2432
- process.stdout.write("(no feedback recorded)\n");
2433
- return 0;
2434
- }
2435
- for (const e of entries) {
2436
- process.stdout.write(`${e.timestamp} ${e.text}
2437
- `);
2438
- }
3000
+ return recordFeedback(opts);
3001
+ }
3002
+ function parseFeedbackOptions(args) {
3003
+ const { json, rest } = consumeJsonFlag(args);
3004
+ const opts = { json, listMode: false, positional: [] };
3005
+ for (const arg of rest) {
3006
+ consumeFeedbackArg(opts, arg);
3007
+ }
3008
+ return opts;
3009
+ }
3010
+ function consumeFeedbackArg(opts, arg) {
3011
+ if (arg === "--list") {
3012
+ opts.listMode = true;
3013
+ return;
3014
+ }
3015
+ if (arg.startsWith("--")) {
3016
+ die(`unknown argument: ${arg}`);
3017
+ }
3018
+ opts.positional.push(arg);
3019
+ }
3020
+ function listFeedback(opts) {
3021
+ if (opts.positional.length > 0) {
3022
+ die("machinen feedback --list takes no positional arguments");
3023
+ }
3024
+ const entries = readFeedback();
3025
+ if (opts.json) {
3026
+ emitJson({ schema_version: 1, entries });
2439
3027
  return 0;
2440
3028
  }
2441
- if (positional.length === 0) {
3029
+ printFeedbackEntries(entries);
3030
+ return 0;
3031
+ }
3032
+ function printFeedbackEntries(entries) {
3033
+ if (entries.length === 0) {
3034
+ process.stdout.write("(no feedback recorded)\n");
3035
+ return;
3036
+ }
3037
+ for (const entry of entries) {
3038
+ process.stdout.write(`${entry.timestamp} ${entry.text}
3039
+ `);
3040
+ }
3041
+ }
3042
+ async function recordFeedback(opts) {
3043
+ if (opts.positional.length === 0) {
2442
3044
  die('usage: machinen feedback "<text>" | machinen feedback --list');
2443
3045
  }
2444
- const text = positional.join(" ");
2445
3046
  const path = feedbackPath();
2446
- const entry = {
3047
+ const entry = newFeedbackEntry(opts.positional.join(" "));
3048
+ appendFeedback(entry, path);
3049
+ const upstream = await postUpstream(entry);
3050
+ reportFeedbackRecorded(opts, path, upstream);
3051
+ return 0;
3052
+ }
3053
+ function newFeedbackEntry(text) {
3054
+ return {
2447
3055
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2448
3056
  cli_version: VERSION,
2449
3057
  text
2450
3058
  };
2451
- appendFeedback(entry, path);
2452
- const upstream = await postUpstream(entry);
2453
- if (json) {
2454
- emitJson({
2455
- schema_version: 1,
2456
- recorded: true,
2457
- path,
2458
- upstream_status: upstream.status
2459
- });
2460
- return 0;
3059
+ }
3060
+ function reportFeedbackRecorded(opts, path, upstream) {
3061
+ if (opts.json) {
3062
+ emitJson({ schema_version: 1, recorded: true, path, upstream_status: upstream.status });
3063
+ return;
2461
3064
  }
3065
+ process.stdout.write(feedbackRecordedMessage(upstream));
3066
+ }
3067
+ function feedbackRecordedMessage(upstream) {
2462
3068
  if (upstream.attempted && upstream.status !== null) {
2463
- process.stdout.write(
2464
- `feedback recorded locally and sent upstream (status: ${upstream.status})
2465
- `
2466
- );
2467
- } else if (upstream.attempted) {
2468
- process.stdout.write(`feedback recorded locally; upstream POST failed: ${upstream.error}
2469
- `);
2470
- } else {
2471
- process.stdout.write("feedback recorded locally (1 entry)\n");
3069
+ return `feedback recorded locally and sent upstream (status: ${upstream.status})
3070
+ `;
2472
3071
  }
2473
- return 0;
3072
+ if (upstream.attempted) {
3073
+ return `feedback recorded locally; upstream POST failed: ${upstream.error}
3074
+ `;
3075
+ }
3076
+ return "feedback recorded locally (1 entry)\n";
2474
3077
  }
2475
3078
  async function cmdCompletion(args) {
2476
3079
  const shell = args[0] ?? "bash";
2477
- if (shell === "bash") {
2478
- process.stdout.write(BASH_COMPLETION);
2479
- return 0;
2480
- }
2481
- if (shell === "zsh") {
2482
- process.stdout.write(ZSH_COMPLETION);
2483
- return 0;
2484
- }
2485
- if (shell === "fish") {
2486
- process.stdout.write(FISH_COMPLETION);
2487
- return 0;
3080
+ const completion = completionForShell(shell);
3081
+ if (completion === void 0) {
3082
+ die(`unsupported shell: ${shell} (expected bash | zsh | fish)`);
2488
3083
  }
2489
- die(`unsupported shell: ${shell} (expected bash | zsh | fish)`);
3084
+ process.stdout.write(completion);
3085
+ return 0;
3086
+ }
3087
+ function completionForShell(shell) {
3088
+ return (/* @__PURE__ */ new Map([
3089
+ ["bash", BASH_COMPLETION],
3090
+ ["zsh", ZSH_COMPLETION],
3091
+ ["fish", FISH_COMPLETION]
3092
+ ])).get(shell);
2490
3093
  }
2491
3094
  function resolveTarget(args, cmd) {
2492
3095
  try {
@@ -2630,6 +3233,8 @@ Usage:
2630
3233
  --env KEY=VALUE Set an env var inside the guest.
2631
3234
  --cwd <abs-path> Start the guest cmd in this directory
2632
3235
  (must be absolute).
3236
+ --nested Expose arm64 EL2 / /dev/kvm to the guest
3237
+ when the host supports it.
2633
3238
  -p <hostPort>:<guestPort> Forward host:hostPort \u2192 guest:guestPort.
2634
3239
 
2635
3240
  machinen restore <snap-dir> [--image <tar.gz>] [--name <name>] [-p ...]
@@ -2664,12 +3269,11 @@ Usage:
2664
3269
  Example:
2665
3270
  machinen exec <name|pid> --tty -- bash -i
2666
3271
  machinen snapshot <name|pid> <out-dir> [--keep-alive]
2667
- CRIU-snapshot a running VM into <d>.
2668
- Default: source VM exits as part of the
2669
- dump. --keep-alive leaves it running
2670
- (and closes inherited TCP sockets to
2671
- avoid two live copies racing on shared
2672
- connection state).
3272
+ Checkpoint a running VM into <d>.
3273
+ Default vmstate snapshots are incremental
3274
+ and non-destructive. CRIU snapshots stay
3275
+ non-incremental; --keep-alive leaves them
3276
+ running and closes inherited TCP sockets.
2673
3277
  machinen fork <name|pid> [--new-name <n>] [--out-dir <d>] [--tcp-keep] [--detach]
2674
3278
  [-p ...] [--mount ...] [--mount-live ...] [--env KEY=VALUE]...
2675
3279
  [--cwd <abs>] [--memory <mib>]
@@ -2736,61 +3340,94 @@ Environment:
2736
3340
  MACHINEN_VMM Override the VMM binary path (dev)
2737
3341
  MACHINEN_ASSETS_DIR Use base assets from this directory
2738
3342
  instead of the cache / GH Releases
3343
+ MACHINEN_GUEST_ARCH Guest asset arch: arm64 or amd64
3344
+ MACHINEN_SNAPSHOT_ENGINE Snapshot engine: vmstate (default),
3345
+ criu, or portable (experimental;
3346
+ unsupported workload today)
2739
3347
  MACHINEN_REGISTRY_DIR Override registry location (default
2740
3348
  ~/.machinen/vms)
2741
3349
 
2742
3350
  Cache:
2743
- ~/.machinen/<tag>/bases/debian-arm64/
3351
+ ~/.machinen/<tag>/bases/debian-arm64/ or debian-amd64/
2744
3352
  `
2745
3353
  );
2746
3354
  }
3355
+ var COMMAND_HANDLERS = /* @__PURE__ */ new Map([
3356
+ ["boot", cmdBoot],
3357
+ ["restore", cmdRestore],
3358
+ ["install", cmdInstall],
3359
+ ["list", cmdLs],
3360
+ ["ls", cmdLs],
3361
+ ["ps", cmdLs],
3362
+ ["exec", cmdExec],
3363
+ ["snapshot", cmdSnapshot],
3364
+ ["fork", cmdFork],
3365
+ ["attach", cmdAttach],
3366
+ ["repl", cmdRepl],
3367
+ ["completion", cmdCompletion],
3368
+ ["gc", cmdGc],
3369
+ ["stop", cmdStop],
3370
+ ["feedback", cmdFeedback],
3371
+ ["agent-context", cmdAgentContext]
3372
+ ]);
2747
3373
  async function main() {
2748
3374
  const [sub, ...rest] = process.argv.slice(2);
2749
- debug("dispatch sub=%s argc=%d", sub ?? "<empty>", rest.length);
2750
- if (!sub || sub === "-h" || sub === "--help") {
3375
+ debug("dispatch sub=%s argc=%d", commandLabel(sub), rest.length);
3376
+ const topLevelCode = maybeHandleTopLevelCommand(sub);
3377
+ if (topLevelCode !== void 0) {
3378
+ return topLevelCode;
3379
+ }
3380
+ return dispatchSubcommand(sub, rest);
3381
+ }
3382
+ function commandLabel(sub) {
3383
+ if (sub === void 0) {
3384
+ return "<empty>";
3385
+ }
3386
+ return sub;
3387
+ }
3388
+ function maybeHandleTopLevelCommand(sub) {
3389
+ const helpCode = maybePrintTopLevelHelp(sub);
3390
+ if (helpCode !== void 0) {
3391
+ return helpCode;
3392
+ }
3393
+ return maybePrintVersion(sub);
3394
+ }
3395
+ function dispatchSubcommand(sub, rest) {
3396
+ const handler = COMMAND_HANDLERS.get(sub);
3397
+ if (handler) {
3398
+ return handler(rest);
3399
+ }
3400
+ die(`unknown command: ${sub}
3401
+ Run 'machinen --help' for usage.`);
3402
+ }
3403
+ function maybePrintTopLevelHelp(sub) {
3404
+ if (!sub) {
2751
3405
  printHelp();
2752
- return sub ? 0 : 1;
3406
+ return 1;
2753
3407
  }
2754
- if (sub === "--version" || sub === "-v") {
2755
- process.stdout.write(`${VERSION}
2756
- `);
3408
+ if (sub === "-h") {
3409
+ printHelp();
2757
3410
  return 0;
2758
3411
  }
2759
- switch (sub) {
2760
- case "boot":
2761
- return cmdBoot(rest);
2762
- case "restore":
2763
- return cmdRestore(rest);
2764
- case "install":
2765
- return cmdInstall(rest);
2766
- case "list":
2767
- case "ls":
2768
- case "ps":
2769
- return cmdLs(rest);
2770
- case "exec":
2771
- return cmdExec(rest);
2772
- case "snapshot":
2773
- return cmdSnapshot(rest);
2774
- case "fork":
2775
- return cmdFork(rest);
2776
- case "attach":
2777
- return cmdAttach(rest);
2778
- case "repl":
2779
- return cmdRepl(rest);
2780
- case "completion":
2781
- return cmdCompletion(rest);
2782
- case "gc":
2783
- return cmdGc(rest);
2784
- case "stop":
2785
- return cmdStop(rest);
2786
- case "feedback":
2787
- return cmdFeedback(rest);
2788
- case "agent-context":
2789
- return cmdAgentContext(rest);
2790
- default:
2791
- die(`unknown command: ${sub}
2792
- Run 'machinen --help' for usage.`);
3412
+ if (sub === "--help") {
3413
+ printHelp();
3414
+ return 0;
2793
3415
  }
3416
+ return void 0;
3417
+ }
3418
+ function maybePrintVersion(sub) {
3419
+ if (sub === "--version") {
3420
+ return printVersion();
3421
+ }
3422
+ if (sub === "-v") {
3423
+ return printVersion();
3424
+ }
3425
+ return void 0;
3426
+ }
3427
+ function printVersion() {
3428
+ process.stdout.write(`${VERSION}
3429
+ `);
3430
+ return 0;
2794
3431
  }
2795
3432
  main().then(
2796
3433
  (code) => process.exit(code),