@revealui/setup 0.3.5 → 0.3.6

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.
@@ -97,8 +97,8 @@ async function bootstrap(options) {
97
97
  name: admin.name ?? "Admin",
98
98
  email: admin.email,
99
99
  password: admin.password,
100
- role: "user-super-admin",
101
- roles: ["user-super-admin"]
100
+ role: "owner",
101
+ roles: ["super-admin"]
102
102
  }
103
103
  });
104
104
  } catch (err) {
@@ -138,7 +138,7 @@ async function bootstrap(options) {
138
138
  return {
139
139
  status: "created",
140
140
  message: `Admin user created${seeded ? " and content seeded" : ""}. Sign in at /admin.`,
141
- user: { email: admin.email, role: "user-super-admin" },
141
+ user: { email: admin.email, role: "owner" },
142
142
  seeded
143
143
  };
144
144
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { BootstrapAdminConfig, BootstrapOptions, BootstrapResult, RevealUILike, bootstrap } from './bootstrap/index.js';
2
2
  export { SetupEnvironmentOptions, SetupEnvironmentResult, generatePassword, generateSecret, parseEnvContent, setupEnvironment, updateEnvValue } from './environment/index.js';
3
+ export { PROFILES, PlanAction, PlatformClass, SystemInfo, TunePlan, TuneProfile, TuneValues, WslConfigTune, detectSystem, generateAutoplan, generatePlan, matchProfile } from './system-tune/index.js';
3
4
  export { LogLevel, Logger, LoggerOptions, createLogger, handleASTParseError, logger } from './utils/index.js';
4
5
  export { EnvVariable, OPTIONAL_ENV_VARS, REQUIRED_ENV_VARS, ValidationResult, validateEnv, validators } from './validators/index.js';
package/dist/index.js CHANGED
@@ -97,8 +97,8 @@ async function bootstrap(options) {
97
97
  name: admin.name ?? "Admin",
98
98
  email: admin.email,
99
99
  password: admin.password,
100
- role: "user-super-admin",
101
- roles: ["user-super-admin"]
100
+ role: "owner",
101
+ roles: ["super-admin"]
102
102
  }
103
103
  });
104
104
  } catch (err) {
@@ -138,7 +138,7 @@ async function bootstrap(options) {
138
138
  return {
139
139
  status: "created",
140
140
  message: `Admin user created${seeded ? " and content seeded" : ""}. Sign in at /admin.`,
141
- user: { email: admin.email, role: "user-super-admin" },
141
+ user: { email: admin.email, role: "owner" },
142
142
  seeded
143
143
  };
144
144
  }
@@ -606,15 +606,367 @@ async function generateSecrets(envPath, logger2) {
606
606
  await writeFile(envPath, content);
607
607
  logger2.success("Generated REVEALUI_SECRET");
608
608
  }
609
+
610
+ // src/system-tune/detect.ts
611
+ import { execSync } from "child_process";
612
+ import { existsSync, readFileSync } from "fs";
613
+ import { arch, cpus, freemem, homedir, platform, release, totalmem } from "os";
614
+ import { join as join2 } from "path";
615
+ function detectPlatformClass() {
616
+ const p = platform();
617
+ if (p === "linux") {
618
+ try {
619
+ const procVersion = readFileSync("/proc/version", "utf8").toLowerCase();
620
+ if (procVersion.includes("microsoft") || procVersion.includes("wsl")) {
621
+ return "wsl2";
622
+ }
623
+ } catch {
624
+ }
625
+ try {
626
+ if (existsSync("/.dockerenv")) return "docker";
627
+ const cgroup = readFileSync("/proc/1/cgroup", "utf8");
628
+ if (cgroup.includes("docker") || cgroup.includes("containerd")) return "docker";
629
+ } catch {
630
+ }
631
+ try {
632
+ const dmiProduct = readFileSync("/sys/class/dmi/id/product_name", "utf8").trim().toLowerCase();
633
+ if (dmiProduct.includes("virtual") || dmiProduct.includes("kvm") || dmiProduct.includes("xen") || dmiProduct.includes("hvm")) {
634
+ return "cloud-vm";
635
+ }
636
+ } catch {
637
+ }
638
+ return "linux";
639
+ }
640
+ if (p === "darwin") return "macos";
641
+ if (p === "win32") return "windows";
642
+ return "unknown";
643
+ }
644
+ function detectDistro() {
645
+ try {
646
+ const osRelease = readFileSync("/etc/os-release", "utf8");
647
+ const match = osRelease.match(/^PRETTY_NAME="?([^"\n]+)"?/m);
648
+ return match?.[1];
649
+ } catch {
650
+ return void 0;
651
+ }
652
+ }
653
+ function detectMemory() {
654
+ const p = platform();
655
+ if (p === "linux") {
656
+ try {
657
+ const meminfo = readFileSync("/proc/meminfo", "utf8");
658
+ const parse = (key) => {
659
+ const match = meminfo.match(new RegExp(`^${key}:\\s+(\\d+)`, "m"));
660
+ return match ? Number.parseInt(match[1], 10) * 1024 : 0;
661
+ };
662
+ return {
663
+ totalBytes: parse("MemTotal"),
664
+ freeBytes: parse("MemAvailable") || parse("MemFree"),
665
+ swapTotalBytes: parse("SwapTotal"),
666
+ swapFreeBytes: parse("SwapFree")
667
+ };
668
+ } catch {
669
+ }
670
+ }
671
+ return {
672
+ totalBytes: totalmem(),
673
+ freeBytes: freemem(),
674
+ swapTotalBytes: 0,
675
+ swapFreeBytes: 0
676
+ };
677
+ }
678
+ function detectCpu() {
679
+ const cores = cpus();
680
+ const model = cores[0]?.model ?? "unknown";
681
+ const logicalCores = cores.length;
682
+ let physicalCores = logicalCores;
683
+ const p = platform();
684
+ if (p === "linux") {
685
+ try {
686
+ const output = execSync('grep "^cpu cores" /proc/cpuinfo | head -1', {
687
+ encoding: "utf8",
688
+ timeout: 2e3
689
+ });
690
+ const match = output.match(/:\s*(\d+)/);
691
+ if (match) physicalCores = Number.parseInt(match[1], 10);
692
+ } catch {
693
+ }
694
+ } else if (p === "darwin") {
695
+ try {
696
+ const output = execSync("sysctl -n hw.physicalcpu", { encoding: "utf8", timeout: 2e3 });
697
+ physicalCores = Number.parseInt(output.trim(), 10) || logicalCores;
698
+ } catch {
699
+ }
700
+ }
701
+ return { model, physicalCores, logicalCores };
702
+ }
703
+ function detectDisk() {
704
+ const p = platform();
705
+ const target = process.cwd();
706
+ if (p === "linux" || p === "darwin") {
707
+ try {
708
+ const output = execSync(`df -B1 "${target}" | tail -1`, { encoding: "utf8", timeout: 2e3 });
709
+ const parts = output.trim().split(/\s+/);
710
+ if (parts.length >= 4) {
711
+ return {
712
+ totalBytes: Number.parseInt(parts[1], 10) || 0,
713
+ freeBytes: Number.parseInt(parts[3], 10) || 0
714
+ };
715
+ }
716
+ } catch {
717
+ }
718
+ }
719
+ return { totalBytes: 0, freeBytes: 0 };
720
+ }
721
+ function detectExistingConfigs() {
722
+ const home = homedir();
723
+ let wslconfig = false;
724
+ try {
725
+ const windowsUser = execSync('cmd.exe /c "echo %USERPROFILE%" 2>/dev/null', {
726
+ encoding: "utf8",
727
+ timeout: 3e3
728
+ }).trim();
729
+ const wslPath = windowsUser.replace(/\\/g, "/").replace(/^([A-Z]):/i, (_, d) => `/mnt/${d.toLowerCase()}`);
730
+ wslconfig = existsSync(join2(wslPath, ".wslconfig"));
731
+ } catch {
732
+ wslconfig = existsSync(join2(home, ".wslconfig"));
733
+ }
734
+ let earlyoom = false;
735
+ try {
736
+ const result = execSync("systemctl is-enabled earlyoom 2>/dev/null", {
737
+ encoding: "utf8",
738
+ timeout: 2e3
739
+ });
740
+ earlyoom = result.trim() === "enabled";
741
+ } catch {
742
+ earlyoom = false;
743
+ }
744
+ let dockerAutostart = false;
745
+ try {
746
+ const result = execSync("systemctl is-enabled docker 2>/dev/null", {
747
+ encoding: "utf8",
748
+ timeout: 2e3
749
+ });
750
+ dockerAutostart = result.trim() === "enabled";
751
+ } catch {
752
+ dockerAutostart = false;
753
+ }
754
+ const nodeOptions = process.env.NODE_OPTIONS ?? null;
755
+ return { wslconfig, earlyoom, dockerAutostart, nodeOptions };
756
+ }
757
+ function detectSystem() {
758
+ return {
759
+ os: {
760
+ platform: platform(),
761
+ release: release(),
762
+ arch: arch(),
763
+ distro: detectDistro()
764
+ },
765
+ platformClass: detectPlatformClass(),
766
+ memory: detectMemory(),
767
+ cpu: detectCpu(),
768
+ disk: detectDisk(),
769
+ existingConfigs: detectExistingConfigs()
770
+ };
771
+ }
772
+
773
+ // src/system-tune/profiles/wsl-low-ram.json
774
+ var wsl_low_ram_default = {
775
+ $schema: "../profile-schema.json",
776
+ id: "wsl-low-ram",
777
+ name: "WSL2 Low-RAM Host",
778
+ description: "Safe defaults for WSL2 on hosts with \u22648 GB RAM. Prevents the VM from starving Windows by capping memory, disabling aggressive reclaim, and keeping Docker off by default.",
779
+ version: "0.1.0",
780
+ origin: "2026-04-13 crash postmortem: WSL on 7.3 GB host claimed ~6 GB, starving Windows. Hand-authored fix captured as seed profile.",
781
+ match: {
782
+ platform: "wsl2",
783
+ maxHostRamGb: 8
784
+ },
785
+ tune: {
786
+ wslconfig: {
787
+ memory: "4GB",
788
+ processors: 2,
789
+ swap: "2GB",
790
+ vmIdleTimeout: -1,
791
+ autoMemoryReclaim: "disabled",
792
+ networkingMode: "mirrored"
793
+ },
794
+ node: {
795
+ maxOldSpaceSize: 2048
796
+ },
797
+ pnpm: {
798
+ childConcurrency: 2
799
+ },
800
+ turbo: {
801
+ concurrency: 2
802
+ },
803
+ vitest: {
804
+ maxThreads: 2
805
+ },
806
+ earlyoom: {
807
+ enabled: true,
808
+ memThreshold: 5,
809
+ swapThreshold: 10,
810
+ prefer: "turbo|biome|vitest|tsc|esbuild"
811
+ },
812
+ docker: {
813
+ autostart: false
814
+ }
815
+ }
816
+ };
817
+
818
+ // src/system-tune/profiles.ts
819
+ var PROFILES = [wsl_low_ram_default];
820
+
821
+ // src/system-tune/plan.ts
822
+ function matchProfile(system) {
823
+ const ramGb = system.memory.totalBytes / (1024 * 1024 * 1024);
824
+ for (const profile of PROFILES) {
825
+ if (profile.match.platform !== system.platformClass) continue;
826
+ if (profile.match.maxHostRamGb !== void 0 && ramGb > profile.match.maxHostRamGb) continue;
827
+ if (profile.match.minHostRamGb !== void 0 && ramGb < profile.match.minHostRamGb) continue;
828
+ return profile;
829
+ }
830
+ return null;
831
+ }
832
+ function generateWslActions(profile, system) {
833
+ const actions = [];
834
+ const wsl = profile.tune.wslconfig;
835
+ if (!wsl) return actions;
836
+ const entries = [];
837
+ if (wsl.memory) entries.push(["memory", wsl.memory]);
838
+ if (wsl.processors !== void 0) entries.push(["processors", String(wsl.processors)]);
839
+ if (wsl.swap) entries.push(["swap", wsl.swap]);
840
+ if (wsl.vmIdleTimeout !== void 0) entries.push(["vmIdleTimeout", String(wsl.vmIdleTimeout)]);
841
+ if (wsl.autoMemoryReclaim) entries.push(["autoMemoryReclaim", wsl.autoMemoryReclaim]);
842
+ if (wsl.networkingMode) entries.push(["networkingMode", wsl.networkingMode]);
843
+ if (entries.length > 0) {
844
+ const desired = entries.map(([k, v]) => `${k}=${v}`).join(", ");
845
+ actions.push({
846
+ target: ".wslconfig [wsl2]",
847
+ current: system.existingConfigs.wslconfig ? "(exists, values unknown)" : null,
848
+ desired,
849
+ privileged: false,
850
+ description: `Set WSL2 config: ${desired}`
851
+ });
852
+ }
853
+ return actions;
854
+ }
855
+ function generateNodeActions(profile, system) {
856
+ const actions = [];
857
+ const node = profile.tune.node;
858
+ if (!node?.maxOldSpaceSize) return actions;
859
+ const desired = `--max-old-space-size=${node.maxOldSpaceSize}`;
860
+ const current = system.existingConfigs.nodeOptions;
861
+ const alreadySet = current?.includes("max-old-space-size") ?? false;
862
+ if (!alreadySet) {
863
+ actions.push({
864
+ target: "NODE_OPTIONS",
865
+ current,
866
+ desired: current ? `${current} ${desired}` : desired,
867
+ privileged: false,
868
+ description: `Add ${desired} to NODE_OPTIONS in shell profile`
869
+ });
870
+ }
871
+ return actions;
872
+ }
873
+ function generateEarlyoomActions(profile, system) {
874
+ const actions = [];
875
+ const oom = profile.tune.earlyoom;
876
+ if (!oom?.enabled) return actions;
877
+ if (!system.existingConfigs.earlyoom) {
878
+ const args = [`-m ${oom.memThreshold ?? 5}`, `-s ${oom.swapThreshold ?? 10}`];
879
+ if (oom.prefer) args.push(`--prefer '${oom.prefer}'`);
880
+ actions.push({
881
+ target: "/etc/default/earlyoom",
882
+ current: null,
883
+ desired: `EARLYOOM_ARGS="${args.join(" ")}"`,
884
+ privileged: true,
885
+ description: `Install and configure earlyoom (kill memory hogs before OOM killer)`
886
+ });
887
+ }
888
+ return actions;
889
+ }
890
+ function generateDockerActions(profile, system) {
891
+ const actions = [];
892
+ const docker = profile.tune.docker;
893
+ if (docker === void 0) return actions;
894
+ if (!docker.autostart && system.existingConfigs.dockerAutostart) {
895
+ actions.push({
896
+ target: "systemctl docker",
897
+ current: "enabled (autostart)",
898
+ desired: "disabled (on-demand via sudo systemctl start docker)",
899
+ privileged: true,
900
+ description: "Disable Docker autostart to save ~200-400 MB RAM on boot"
901
+ });
902
+ }
903
+ return actions;
904
+ }
905
+ function generateConcurrencyActions(profile) {
906
+ const actions = [];
907
+ if (profile.tune.pnpm?.childConcurrency) {
908
+ actions.push({
909
+ target: ".npmrc or pnpm config",
910
+ current: null,
911
+ desired: `child-concurrency=${profile.tune.pnpm.childConcurrency}`,
912
+ privileged: false,
913
+ description: `Limit pnpm child concurrency to ${profile.tune.pnpm.childConcurrency}`
914
+ });
915
+ }
916
+ if (profile.tune.turbo?.concurrency) {
917
+ actions.push({
918
+ target: "turbo.json or TURBO_CONCURRENCY",
919
+ current: null,
920
+ desired: `concurrency=${profile.tune.turbo.concurrency}`,
921
+ privileged: false,
922
+ description: `Limit Turbo concurrency to ${profile.tune.turbo.concurrency}`
923
+ });
924
+ }
925
+ if (profile.tune.vitest?.maxThreads) {
926
+ actions.push({
927
+ target: "vitest.config poolOptions.threads.maxThreads",
928
+ current: null,
929
+ desired: `maxThreads=${profile.tune.vitest.maxThreads}`,
930
+ privileged: false,
931
+ description: `Limit Vitest threads to ${profile.tune.vitest.maxThreads}`
932
+ });
933
+ }
934
+ return actions;
935
+ }
936
+ function generatePlan(system, profile) {
937
+ const actions = [
938
+ ...generateWslActions(profile, system),
939
+ ...generateNodeActions(profile, system),
940
+ ...generateEarlyoomActions(profile, system),
941
+ ...generateDockerActions(profile, system),
942
+ ...generateConcurrencyActions(profile)
943
+ ];
944
+ return {
945
+ profileId: profile.id,
946
+ system,
947
+ actions,
948
+ isNoop: actions.length === 0
949
+ };
950
+ }
951
+ function generateAutoplan(system) {
952
+ const profile = matchProfile(system);
953
+ if (!profile) return null;
954
+ return generatePlan(system, profile);
955
+ }
609
956
  export {
610
957
  OPTIONAL_ENV_VARS,
958
+ PROFILES,
611
959
  REQUIRED_ENV_VARS,
612
960
  bootstrap,
613
961
  createLogger,
962
+ detectSystem,
963
+ generateAutoplan,
614
964
  generatePassword,
965
+ generatePlan,
615
966
  generateSecret,
616
967
  handleASTParseError,
617
968
  logger,
969
+ matchProfile,
618
970
  parseEnvContent,
619
971
  setupEnvironment,
620
972
  updateEnvValue,
@@ -0,0 +1,152 @@
1
+ /**
2
+ * System Tune Types
3
+ *
4
+ * Shared types for hardware detection, tuning profiles, and plan generation.
5
+ */
6
+ type PlatformClass = 'wsl2' | 'linux' | 'macos' | 'windows' | 'docker' | 'cloud-vm' | 'unknown';
7
+ interface SystemInfo {
8
+ /** OS/distro/kernel identification */
9
+ os: {
10
+ platform: NodeJS.Platform;
11
+ release: string;
12
+ arch: string;
13
+ distro?: string;
14
+ };
15
+ /** Detected platform class */
16
+ platformClass: PlatformClass;
17
+ /** Memory in bytes */
18
+ memory: {
19
+ totalBytes: number;
20
+ freeBytes: number;
21
+ swapTotalBytes: number;
22
+ swapFreeBytes: number;
23
+ };
24
+ /** CPU info */
25
+ cpu: {
26
+ model: string;
27
+ physicalCores: number;
28
+ logicalCores: number;
29
+ };
30
+ /** Disk info for the working directory */
31
+ disk: {
32
+ totalBytes: number;
33
+ freeBytes: number;
34
+ };
35
+ /** Existing configs already present */
36
+ existingConfigs: {
37
+ wslconfig: boolean;
38
+ earlyoom: boolean;
39
+ dockerAutostart: boolean;
40
+ nodeOptions: string | null;
41
+ };
42
+ }
43
+ interface WslConfigTune {
44
+ memory?: string;
45
+ processors?: number;
46
+ swap?: string;
47
+ vmIdleTimeout?: number;
48
+ autoMemoryReclaim?: string;
49
+ networkingMode?: string;
50
+ }
51
+ interface TuneValues {
52
+ wslconfig?: WslConfigTune;
53
+ node?: {
54
+ maxOldSpaceSize?: number;
55
+ };
56
+ pnpm?: {
57
+ childConcurrency?: number;
58
+ };
59
+ turbo?: {
60
+ concurrency?: number;
61
+ };
62
+ vitest?: {
63
+ maxThreads?: number;
64
+ };
65
+ earlyoom?: {
66
+ enabled?: boolean;
67
+ memThreshold?: number;
68
+ swapThreshold?: number;
69
+ prefer?: string;
70
+ };
71
+ docker?: {
72
+ autostart?: boolean;
73
+ };
74
+ macos?: {
75
+ maxFiles?: number;
76
+ spotlightExclusions?: string[];
77
+ };
78
+ windows?: {
79
+ defenderExclusions?: string[];
80
+ longPaths?: boolean;
81
+ };
82
+ }
83
+ interface TuneProfile {
84
+ id: string;
85
+ name: string;
86
+ description: string;
87
+ version: string;
88
+ origin?: string;
89
+ match: {
90
+ platform: PlatformClass;
91
+ maxHostRamGb?: number;
92
+ minHostRamGb?: number;
93
+ };
94
+ tune: TuneValues;
95
+ }
96
+ interface PlanAction {
97
+ /** What file or setting is being changed */
98
+ target: string;
99
+ /** What the current value is (null if not set) */
100
+ current: string | null;
101
+ /** What the new value will be */
102
+ desired: string;
103
+ /** Whether this requires elevated privileges */
104
+ privileged: boolean;
105
+ /** Human-readable description */
106
+ description: string;
107
+ }
108
+ interface TunePlan {
109
+ /** Profile that generated this plan */
110
+ profileId: string;
111
+ /** Detected system info */
112
+ system: SystemInfo;
113
+ /** Actions to take */
114
+ actions: PlanAction[];
115
+ /** Whether all actions are no-ops (system already tuned) */
116
+ isNoop: boolean;
117
+ }
118
+
119
+ /**
120
+ * System Detection Layer
121
+ *
122
+ * Scans the host's hardware and platform to produce a SystemInfo snapshot.
123
+ * This is the "read" side — no mutations, only observation.
124
+ */
125
+
126
+ /** Scan the current host and return a SystemInfo snapshot. */
127
+ declare function detectSystem(): SystemInfo;
128
+
129
+ /**
130
+ * Plan Generator
131
+ *
132
+ * Pure function: (detected system state, profile) → plan of actions.
133
+ * No I/O — only computes the diff between current and desired state.
134
+ */
135
+
136
+ /** Find the best matching profile for the detected system. */
137
+ declare function matchProfile(system: SystemInfo): TuneProfile | null;
138
+ /** Generate a tuning plan from detected system state and a profile. */
139
+ declare function generatePlan(system: SystemInfo, profile: TuneProfile): TunePlan;
140
+ /** Detect system, find matching profile, and generate plan. */
141
+ declare function generateAutoplan(system: SystemInfo): TunePlan | null;
142
+
143
+ /**
144
+ * Built-in Tuning Profiles
145
+ *
146
+ * Profiles are ordered by specificity — the first match wins.
147
+ * Add new profiles here, most specific first.
148
+ */
149
+
150
+ declare const PROFILES: TuneProfile[];
151
+
152
+ export { PROFILES, type PlanAction, type PlatformClass, type SystemInfo, type TunePlan, type TuneProfile, type TuneValues, type WslConfigTune, detectSystem, generateAutoplan, generatePlan, matchProfile };
@@ -0,0 +1,353 @@
1
+ // src/system-tune/detect.ts
2
+ import { execSync } from "child_process";
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { arch, cpus, freemem, homedir, platform, release, totalmem } from "os";
5
+ import { join } from "path";
6
+ function detectPlatformClass() {
7
+ const p = platform();
8
+ if (p === "linux") {
9
+ try {
10
+ const procVersion = readFileSync("/proc/version", "utf8").toLowerCase();
11
+ if (procVersion.includes("microsoft") || procVersion.includes("wsl")) {
12
+ return "wsl2";
13
+ }
14
+ } catch {
15
+ }
16
+ try {
17
+ if (existsSync("/.dockerenv")) return "docker";
18
+ const cgroup = readFileSync("/proc/1/cgroup", "utf8");
19
+ if (cgroup.includes("docker") || cgroup.includes("containerd")) return "docker";
20
+ } catch {
21
+ }
22
+ try {
23
+ const dmiProduct = readFileSync("/sys/class/dmi/id/product_name", "utf8").trim().toLowerCase();
24
+ if (dmiProduct.includes("virtual") || dmiProduct.includes("kvm") || dmiProduct.includes("xen") || dmiProduct.includes("hvm")) {
25
+ return "cloud-vm";
26
+ }
27
+ } catch {
28
+ }
29
+ return "linux";
30
+ }
31
+ if (p === "darwin") return "macos";
32
+ if (p === "win32") return "windows";
33
+ return "unknown";
34
+ }
35
+ function detectDistro() {
36
+ try {
37
+ const osRelease = readFileSync("/etc/os-release", "utf8");
38
+ const match = osRelease.match(/^PRETTY_NAME="?([^"\n]+)"?/m);
39
+ return match?.[1];
40
+ } catch {
41
+ return void 0;
42
+ }
43
+ }
44
+ function detectMemory() {
45
+ const p = platform();
46
+ if (p === "linux") {
47
+ try {
48
+ const meminfo = readFileSync("/proc/meminfo", "utf8");
49
+ const parse = (key) => {
50
+ const match = meminfo.match(new RegExp(`^${key}:\\s+(\\d+)`, "m"));
51
+ return match ? Number.parseInt(match[1], 10) * 1024 : 0;
52
+ };
53
+ return {
54
+ totalBytes: parse("MemTotal"),
55
+ freeBytes: parse("MemAvailable") || parse("MemFree"),
56
+ swapTotalBytes: parse("SwapTotal"),
57
+ swapFreeBytes: parse("SwapFree")
58
+ };
59
+ } catch {
60
+ }
61
+ }
62
+ return {
63
+ totalBytes: totalmem(),
64
+ freeBytes: freemem(),
65
+ swapTotalBytes: 0,
66
+ swapFreeBytes: 0
67
+ };
68
+ }
69
+ function detectCpu() {
70
+ const cores = cpus();
71
+ const model = cores[0]?.model ?? "unknown";
72
+ const logicalCores = cores.length;
73
+ let physicalCores = logicalCores;
74
+ const p = platform();
75
+ if (p === "linux") {
76
+ try {
77
+ const output = execSync('grep "^cpu cores" /proc/cpuinfo | head -1', {
78
+ encoding: "utf8",
79
+ timeout: 2e3
80
+ });
81
+ const match = output.match(/:\s*(\d+)/);
82
+ if (match) physicalCores = Number.parseInt(match[1], 10);
83
+ } catch {
84
+ }
85
+ } else if (p === "darwin") {
86
+ try {
87
+ const output = execSync("sysctl -n hw.physicalcpu", { encoding: "utf8", timeout: 2e3 });
88
+ physicalCores = Number.parseInt(output.trim(), 10) || logicalCores;
89
+ } catch {
90
+ }
91
+ }
92
+ return { model, physicalCores, logicalCores };
93
+ }
94
+ function detectDisk() {
95
+ const p = platform();
96
+ const target = process.cwd();
97
+ if (p === "linux" || p === "darwin") {
98
+ try {
99
+ const output = execSync(`df -B1 "${target}" | tail -1`, { encoding: "utf8", timeout: 2e3 });
100
+ const parts = output.trim().split(/\s+/);
101
+ if (parts.length >= 4) {
102
+ return {
103
+ totalBytes: Number.parseInt(parts[1], 10) || 0,
104
+ freeBytes: Number.parseInt(parts[3], 10) || 0
105
+ };
106
+ }
107
+ } catch {
108
+ }
109
+ }
110
+ return { totalBytes: 0, freeBytes: 0 };
111
+ }
112
+ function detectExistingConfigs() {
113
+ const home = homedir();
114
+ let wslconfig = false;
115
+ try {
116
+ const windowsUser = execSync('cmd.exe /c "echo %USERPROFILE%" 2>/dev/null', {
117
+ encoding: "utf8",
118
+ timeout: 3e3
119
+ }).trim();
120
+ const wslPath = windowsUser.replace(/\\/g, "/").replace(/^([A-Z]):/i, (_, d) => `/mnt/${d.toLowerCase()}`);
121
+ wslconfig = existsSync(join(wslPath, ".wslconfig"));
122
+ } catch {
123
+ wslconfig = existsSync(join(home, ".wslconfig"));
124
+ }
125
+ let earlyoom = false;
126
+ try {
127
+ const result = execSync("systemctl is-enabled earlyoom 2>/dev/null", {
128
+ encoding: "utf8",
129
+ timeout: 2e3
130
+ });
131
+ earlyoom = result.trim() === "enabled";
132
+ } catch {
133
+ earlyoom = false;
134
+ }
135
+ let dockerAutostart = false;
136
+ try {
137
+ const result = execSync("systemctl is-enabled docker 2>/dev/null", {
138
+ encoding: "utf8",
139
+ timeout: 2e3
140
+ });
141
+ dockerAutostart = result.trim() === "enabled";
142
+ } catch {
143
+ dockerAutostart = false;
144
+ }
145
+ const nodeOptions = process.env.NODE_OPTIONS ?? null;
146
+ return { wslconfig, earlyoom, dockerAutostart, nodeOptions };
147
+ }
148
+ function detectSystem() {
149
+ return {
150
+ os: {
151
+ platform: platform(),
152
+ release: release(),
153
+ arch: arch(),
154
+ distro: detectDistro()
155
+ },
156
+ platformClass: detectPlatformClass(),
157
+ memory: detectMemory(),
158
+ cpu: detectCpu(),
159
+ disk: detectDisk(),
160
+ existingConfigs: detectExistingConfigs()
161
+ };
162
+ }
163
+
164
+ // src/system-tune/profiles/wsl-low-ram.json
165
+ var wsl_low_ram_default = {
166
+ $schema: "../profile-schema.json",
167
+ id: "wsl-low-ram",
168
+ name: "WSL2 Low-RAM Host",
169
+ description: "Safe defaults for WSL2 on hosts with \u22648 GB RAM. Prevents the VM from starving Windows by capping memory, disabling aggressive reclaim, and keeping Docker off by default.",
170
+ version: "0.1.0",
171
+ origin: "2026-04-13 crash postmortem: WSL on 7.3 GB host claimed ~6 GB, starving Windows. Hand-authored fix captured as seed profile.",
172
+ match: {
173
+ platform: "wsl2",
174
+ maxHostRamGb: 8
175
+ },
176
+ tune: {
177
+ wslconfig: {
178
+ memory: "4GB",
179
+ processors: 2,
180
+ swap: "2GB",
181
+ vmIdleTimeout: -1,
182
+ autoMemoryReclaim: "disabled",
183
+ networkingMode: "mirrored"
184
+ },
185
+ node: {
186
+ maxOldSpaceSize: 2048
187
+ },
188
+ pnpm: {
189
+ childConcurrency: 2
190
+ },
191
+ turbo: {
192
+ concurrency: 2
193
+ },
194
+ vitest: {
195
+ maxThreads: 2
196
+ },
197
+ earlyoom: {
198
+ enabled: true,
199
+ memThreshold: 5,
200
+ swapThreshold: 10,
201
+ prefer: "turbo|biome|vitest|tsc|esbuild"
202
+ },
203
+ docker: {
204
+ autostart: false
205
+ }
206
+ }
207
+ };
208
+
209
+ // src/system-tune/profiles.ts
210
+ var PROFILES = [wsl_low_ram_default];
211
+
212
+ // src/system-tune/plan.ts
213
+ function matchProfile(system) {
214
+ const ramGb = system.memory.totalBytes / (1024 * 1024 * 1024);
215
+ for (const profile of PROFILES) {
216
+ if (profile.match.platform !== system.platformClass) continue;
217
+ if (profile.match.maxHostRamGb !== void 0 && ramGb > profile.match.maxHostRamGb) continue;
218
+ if (profile.match.minHostRamGb !== void 0 && ramGb < profile.match.minHostRamGb) continue;
219
+ return profile;
220
+ }
221
+ return null;
222
+ }
223
+ function generateWslActions(profile, system) {
224
+ const actions = [];
225
+ const wsl = profile.tune.wslconfig;
226
+ if (!wsl) return actions;
227
+ const entries = [];
228
+ if (wsl.memory) entries.push(["memory", wsl.memory]);
229
+ if (wsl.processors !== void 0) entries.push(["processors", String(wsl.processors)]);
230
+ if (wsl.swap) entries.push(["swap", wsl.swap]);
231
+ if (wsl.vmIdleTimeout !== void 0) entries.push(["vmIdleTimeout", String(wsl.vmIdleTimeout)]);
232
+ if (wsl.autoMemoryReclaim) entries.push(["autoMemoryReclaim", wsl.autoMemoryReclaim]);
233
+ if (wsl.networkingMode) entries.push(["networkingMode", wsl.networkingMode]);
234
+ if (entries.length > 0) {
235
+ const desired = entries.map(([k, v]) => `${k}=${v}`).join(", ");
236
+ actions.push({
237
+ target: ".wslconfig [wsl2]",
238
+ current: system.existingConfigs.wslconfig ? "(exists, values unknown)" : null,
239
+ desired,
240
+ privileged: false,
241
+ description: `Set WSL2 config: ${desired}`
242
+ });
243
+ }
244
+ return actions;
245
+ }
246
+ function generateNodeActions(profile, system) {
247
+ const actions = [];
248
+ const node = profile.tune.node;
249
+ if (!node?.maxOldSpaceSize) return actions;
250
+ const desired = `--max-old-space-size=${node.maxOldSpaceSize}`;
251
+ const current = system.existingConfigs.nodeOptions;
252
+ const alreadySet = current?.includes("max-old-space-size") ?? false;
253
+ if (!alreadySet) {
254
+ actions.push({
255
+ target: "NODE_OPTIONS",
256
+ current,
257
+ desired: current ? `${current} ${desired}` : desired,
258
+ privileged: false,
259
+ description: `Add ${desired} to NODE_OPTIONS in shell profile`
260
+ });
261
+ }
262
+ return actions;
263
+ }
264
+ function generateEarlyoomActions(profile, system) {
265
+ const actions = [];
266
+ const oom = profile.tune.earlyoom;
267
+ if (!oom?.enabled) return actions;
268
+ if (!system.existingConfigs.earlyoom) {
269
+ const args = [`-m ${oom.memThreshold ?? 5}`, `-s ${oom.swapThreshold ?? 10}`];
270
+ if (oom.prefer) args.push(`--prefer '${oom.prefer}'`);
271
+ actions.push({
272
+ target: "/etc/default/earlyoom",
273
+ current: null,
274
+ desired: `EARLYOOM_ARGS="${args.join(" ")}"`,
275
+ privileged: true,
276
+ description: `Install and configure earlyoom (kill memory hogs before OOM killer)`
277
+ });
278
+ }
279
+ return actions;
280
+ }
281
+ function generateDockerActions(profile, system) {
282
+ const actions = [];
283
+ const docker = profile.tune.docker;
284
+ if (docker === void 0) return actions;
285
+ if (!docker.autostart && system.existingConfigs.dockerAutostart) {
286
+ actions.push({
287
+ target: "systemctl docker",
288
+ current: "enabled (autostart)",
289
+ desired: "disabled (on-demand via sudo systemctl start docker)",
290
+ privileged: true,
291
+ description: "Disable Docker autostart to save ~200-400 MB RAM on boot"
292
+ });
293
+ }
294
+ return actions;
295
+ }
296
+ function generateConcurrencyActions(profile) {
297
+ const actions = [];
298
+ if (profile.tune.pnpm?.childConcurrency) {
299
+ actions.push({
300
+ target: ".npmrc or pnpm config",
301
+ current: null,
302
+ desired: `child-concurrency=${profile.tune.pnpm.childConcurrency}`,
303
+ privileged: false,
304
+ description: `Limit pnpm child concurrency to ${profile.tune.pnpm.childConcurrency}`
305
+ });
306
+ }
307
+ if (profile.tune.turbo?.concurrency) {
308
+ actions.push({
309
+ target: "turbo.json or TURBO_CONCURRENCY",
310
+ current: null,
311
+ desired: `concurrency=${profile.tune.turbo.concurrency}`,
312
+ privileged: false,
313
+ description: `Limit Turbo concurrency to ${profile.tune.turbo.concurrency}`
314
+ });
315
+ }
316
+ if (profile.tune.vitest?.maxThreads) {
317
+ actions.push({
318
+ target: "vitest.config poolOptions.threads.maxThreads",
319
+ current: null,
320
+ desired: `maxThreads=${profile.tune.vitest.maxThreads}`,
321
+ privileged: false,
322
+ description: `Limit Vitest threads to ${profile.tune.vitest.maxThreads}`
323
+ });
324
+ }
325
+ return actions;
326
+ }
327
+ function generatePlan(system, profile) {
328
+ const actions = [
329
+ ...generateWslActions(profile, system),
330
+ ...generateNodeActions(profile, system),
331
+ ...generateEarlyoomActions(profile, system),
332
+ ...generateDockerActions(profile, system),
333
+ ...generateConcurrencyActions(profile)
334
+ ];
335
+ return {
336
+ profileId: profile.id,
337
+ system,
338
+ actions,
339
+ isNoop: actions.length === 0
340
+ };
341
+ }
342
+ function generateAutoplan(system) {
343
+ const profile = matchProfile(system);
344
+ if (!profile) return null;
345
+ return generatePlan(system, profile);
346
+ }
347
+ export {
348
+ PROFILES,
349
+ detectSystem,
350
+ generateAutoplan,
351
+ generatePlan,
352
+ matchProfile
353
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revealui/setup",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "Setup utilities for RevealUI projects - environment, database, and configuration management",
5
5
  "keywords": [
6
6
  "cli",
@@ -22,7 +22,7 @@
22
22
  "inquirer": "^13.4.1",
23
23
  "ora": "^9.3.0",
24
24
  "zod": "^4.3.6",
25
- "@revealui/config": "0.3.4"
25
+ "@revealui/config": "0.4.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/inquirer": "^9.0.9",
@@ -54,6 +54,10 @@
54
54
  "./bootstrap": {
55
55
  "types": "./dist/bootstrap/index.d.ts",
56
56
  "import": "./dist/bootstrap/index.js"
57
+ },
58
+ "./system-tune": {
59
+ "types": "./dist/system-tune/index.d.ts",
60
+ "import": "./dist/system-tune/index.js"
57
61
  }
58
62
  },
59
63
  "files": [