@phystack/cli 4.3.40-dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/LICENSE.md +19 -0
  2. package/README.md +24 -0
  3. package/bin/index.js +2 -0
  4. package/dist/commands/app/build-apps.js +66 -0
  5. package/dist/commands/app/build-apps.js.map +1 -0
  6. package/dist/commands/app/build-container-remote.js +171 -0
  7. package/dist/commands/app/build-container-remote.js.map +1 -0
  8. package/dist/commands/app/build-container.js +330 -0
  9. package/dist/commands/app/build-container.js.map +1 -0
  10. package/dist/commands/app/build.js +393 -0
  11. package/dist/commands/app/build.js.map +1 -0
  12. package/dist/commands/app/create.js +451 -0
  13. package/dist/commands/app/create.js.map +1 -0
  14. package/dist/commands/app/deploy.js +176 -0
  15. package/dist/commands/app/deploy.js.map +1 -0
  16. package/dist/commands/app/device-picker.js +150 -0
  17. package/dist/commands/app/device-picker.js.map +1 -0
  18. package/dist/commands/app/import.js +303 -0
  19. package/dist/commands/app/import.js.map +1 -0
  20. package/dist/commands/app/list.js +26 -0
  21. package/dist/commands/app/list.js.map +1 -0
  22. package/dist/commands/app/publish.js +327 -0
  23. package/dist/commands/app/publish.js.map +1 -0
  24. package/dist/commands/app/settings.js +129 -0
  25. package/dist/commands/app/settings.js.map +1 -0
  26. package/dist/commands/app/types.js +13 -0
  27. package/dist/commands/app/types.js.map +1 -0
  28. package/dist/commands/app/upload-description.js +139 -0
  29. package/dist/commands/app/upload-description.js.map +1 -0
  30. package/dist/commands/app/upload-settings.js +29 -0
  31. package/dist/commands/app/upload-settings.js.map +1 -0
  32. package/dist/commands/app/utils.js +86 -0
  33. package/dist/commands/app/utils.js.map +1 -0
  34. package/dist/commands/auth/login.js +111 -0
  35. package/dist/commands/auth/login.js.map +1 -0
  36. package/dist/commands/auth/logout.js +19 -0
  37. package/dist/commands/auth/logout.js.map +1 -0
  38. package/dist/commands/descriptor/create.js +143 -0
  39. package/dist/commands/descriptor/create.js.map +1 -0
  40. package/dist/commands/descriptor/index.js +36 -0
  41. package/dist/commands/descriptor/index.js.map +1 -0
  42. package/dist/commands/descriptor/publish.js +163 -0
  43. package/dist/commands/descriptor/publish.js.map +1 -0
  44. package/dist/commands/descriptor/show.js +68 -0
  45. package/dist/commands/descriptor/show.js.map +1 -0
  46. package/dist/commands/dev/develop.js +175 -0
  47. package/dist/commands/dev/develop.js.map +1 -0
  48. package/dist/commands/dev/forward.js +118 -0
  49. package/dist/commands/dev/forward.js.map +1 -0
  50. package/dist/commands/dev/index.js +66 -0
  51. package/dist/commands/dev/index.js.map +1 -0
  52. package/dist/commands/dev/list.js +96 -0
  53. package/dist/commands/dev/list.js.map +1 -0
  54. package/dist/commands/dev/screen-devtools.js +156 -0
  55. package/dist/commands/dev/screen-devtools.js.map +1 -0
  56. package/dist/commands/dev/select.js +118 -0
  57. package/dist/commands/dev/select.js.map +1 -0
  58. package/dist/commands/dev/shell.js +171 -0
  59. package/dist/commands/dev/shell.js.map +1 -0
  60. package/dist/commands/dev/vnc.js +75 -0
  61. package/dist/commands/dev/vnc.js.map +1 -0
  62. package/dist/commands/device/select.js +118 -0
  63. package/dist/commands/device/select.js.map +1 -0
  64. package/dist/commands/flash.js +1120 -0
  65. package/dist/commands/flash.js.map +1 -0
  66. package/dist/commands/inst/create.js +55 -0
  67. package/dist/commands/inst/create.js.map +1 -0
  68. package/dist/commands/inst/index.js +15 -0
  69. package/dist/commands/inst/index.js.map +1 -0
  70. package/dist/commands/inst/list.js +26 -0
  71. package/dist/commands/inst/list.js.map +1 -0
  72. package/dist/commands/legacydev/debug.js +11 -0
  73. package/dist/commands/legacydev/debug.js.map +1 -0
  74. package/dist/commands/legacydev/deploy.js +15 -0
  75. package/dist/commands/legacydev/deploy.js.map +1 -0
  76. package/dist/commands/legacydev/dumpTwin.js +27 -0
  77. package/dist/commands/legacydev/dumpTwin.js.map +1 -0
  78. package/dist/commands/legacydev/forward.js +104 -0
  79. package/dist/commands/legacydev/forward.js.map +1 -0
  80. package/dist/commands/legacydev/index.js +188 -0
  81. package/dist/commands/legacydev/index.js.map +1 -0
  82. package/dist/commands/legacydev/invoke.js +29 -0
  83. package/dist/commands/legacydev/invoke.js.map +1 -0
  84. package/dist/commands/legacydev/js.js +69 -0
  85. package/dist/commands/legacydev/js.js.map +1 -0
  86. package/dist/commands/legacydev/list.js +196 -0
  87. package/dist/commands/legacydev/list.js.map +1 -0
  88. package/dist/commands/legacydev/logs.js +60 -0
  89. package/dist/commands/legacydev/logs.js.map +1 -0
  90. package/dist/commands/legacydev/modules.js +50 -0
  91. package/dist/commands/legacydev/modules.js.map +1 -0
  92. package/dist/commands/legacydev/move.js +23 -0
  93. package/dist/commands/legacydev/move.js.map +1 -0
  94. package/dist/commands/legacydev/ota.js +88 -0
  95. package/dist/commands/legacydev/ota.js.map +1 -0
  96. package/dist/commands/legacydev/patchTwin.js +21 -0
  97. package/dist/commands/legacydev/patchTwin.js.map +1 -0
  98. package/dist/commands/legacydev/pin.js +23 -0
  99. package/dist/commands/legacydev/pin.js.map +1 -0
  100. package/dist/commands/legacydev/pub.js +25 -0
  101. package/dist/commands/legacydev/pub.js.map +1 -0
  102. package/dist/commands/legacydev/rdp.js +64 -0
  103. package/dist/commands/legacydev/rdp.js.map +1 -0
  104. package/dist/commands/legacydev/screen-devtools.js +142 -0
  105. package/dist/commands/legacydev/screen-devtools.js.map +1 -0
  106. package/dist/commands/legacydev/settingsShow.js +89 -0
  107. package/dist/commands/legacydev/settingsShow.js.map +1 -0
  108. package/dist/commands/legacydev/settingsUpdate.js +114 -0
  109. package/dist/commands/legacydev/settingsUpdate.js.map +1 -0
  110. package/dist/commands/legacydev/shell.js +167 -0
  111. package/dist/commands/legacydev/shell.js.map +1 -0
  112. package/dist/commands/legacydev/showTwin.js +9 -0
  113. package/dist/commands/legacydev/showTwin.js.map +1 -0
  114. package/dist/commands/legacydev/statusLog.js +56 -0
  115. package/dist/commands/legacydev/statusLog.js.map +1 -0
  116. package/dist/commands/legacydev/sub.js +39 -0
  117. package/dist/commands/legacydev/sub.js.map +1 -0
  118. package/dist/commands/legacydev/vnc.js +61 -0
  119. package/dist/commands/legacydev/vnc.js.map +1 -0
  120. package/dist/commands/tenant/index.js +21 -0
  121. package/dist/commands/tenant/index.js.map +1 -0
  122. package/dist/commands/tenant/list.js +14 -0
  123. package/dist/commands/tenant/list.js.map +1 -0
  124. package/dist/commands/tenant/select.js +87 -0
  125. package/dist/commands/tenant/select.js.map +1 -0
  126. package/dist/commands/vm/create.js +718 -0
  127. package/dist/commands/vm/create.js.map +1 -0
  128. package/dist/commands/vm/index.js +130 -0
  129. package/dist/commands/vm/index.js.map +1 -0
  130. package/dist/commands/vm/list.js +124 -0
  131. package/dist/commands/vm/list.js.map +1 -0
  132. package/dist/commands/vm/logs.js +66 -0
  133. package/dist/commands/vm/logs.js.map +1 -0
  134. package/dist/commands/vm/remove.js +180 -0
  135. package/dist/commands/vm/remove.js.map +1 -0
  136. package/dist/commands/vm/shell.js +400 -0
  137. package/dist/commands/vm/shell.js.map +1 -0
  138. package/dist/commands/vm/start.js +861 -0
  139. package/dist/commands/vm/start.js.map +1 -0
  140. package/dist/commands/vm/stop.js +232 -0
  141. package/dist/commands/vm/stop.js.map +1 -0
  142. package/dist/index.js +158 -0
  143. package/dist/index.js.map +1 -0
  144. package/dist/services/admin-api/admin-api.types.js +3 -0
  145. package/dist/services/admin-api/admin-api.types.js.map +1 -0
  146. package/dist/services/admin-api/device-modules.admin-api.service.js +58 -0
  147. package/dist/services/admin-api/device-modules.admin-api.service.js.map +1 -0
  148. package/dist/services/admin-api/devices-admin-api.service.js +213 -0
  149. package/dist/services/admin-api/devices-admin-api.service.js.map +1 -0
  150. package/dist/services/admin-api/gridapps-admin-api.service.js +59 -0
  151. package/dist/services/admin-api/gridapps-admin-api.service.js.map +1 -0
  152. package/dist/services/admin-api/index.js +157 -0
  153. package/dist/services/admin-api/index.js.map +1 -0
  154. package/dist/services/admin-api/installations-admin-api.service.js +29 -0
  155. package/dist/services/admin-api/installations-admin-api.service.js.map +1 -0
  156. package/dist/services/admin-api/organizations-admin-api.service.js +53 -0
  157. package/dist/services/admin-api/organizations-admin-api.service.js.map +1 -0
  158. package/dist/services/auth/device-grant-auth.service.js +224 -0
  159. package/dist/services/auth/device-grant-auth.service.js.map +1 -0
  160. package/dist/services/phyhub/index.js +200 -0
  161. package/dist/services/phyhub/index.js.map +1 -0
  162. package/dist/services/phyhub/phyhub.types.js +3 -0
  163. package/dist/services/phyhub/phyhub.types.js.map +1 -0
  164. package/dist/utils/device-fetcher.js +92 -0
  165. package/dist/utils/device-fetcher.js.map +1 -0
  166. package/dist/utils/devices.js +41 -0
  167. package/dist/utils/devices.js.map +1 -0
  168. package/dist/utils/docker-credentials.js +720 -0
  169. package/dist/utils/docker-credentials.js.map +1 -0
  170. package/dist/utils/emulated-device.js +91 -0
  171. package/dist/utils/emulated-device.js.map +1 -0
  172. package/dist/utils/index.js +180 -0
  173. package/dist/utils/index.js.map +1 -0
  174. package/dist/utils/modules.js +36 -0
  175. package/dist/utils/modules.js.map +1 -0
  176. package/dist/utils/org-selector.js +108 -0
  177. package/dist/utils/org-selector.js.map +1 -0
  178. package/dist/utils/proxy.js +31 -0
  179. package/dist/utils/proxy.js.map +1 -0
  180. package/dist/utils/registry-credentials.js +113 -0
  181. package/dist/utils/registry-credentials.js.map +1 -0
  182. package/dist/utils/statuses.js +124 -0
  183. package/dist/utils/statuses.js.map +1 -0
  184. package/dist/utils/templates.js +197 -0
  185. package/dist/utils/templates.js.map +1 -0
  186. package/dist/utils/tenant-storage.js +88 -0
  187. package/dist/utils/tenant-storage.js.map +1 -0
  188. package/dist/utils/vm.js +434 -0
  189. package/dist/utils/vm.js.map +1 -0
  190. package/dist/utils/with-spinner.js +20 -0
  191. package/dist/utils/with-spinner.js.map +1 -0
  192. package/package.json +103 -0
@@ -0,0 +1,861 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = startVm;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ const child_process_1 = require("child_process");
11
+ const vm_1 = require("../../utils/vm");
12
+ const inquirer_1 = __importDefault(require("inquirer"));
13
+ /**
14
+ * Starts a single PhyOS VM with QEMU (helper function)
15
+ */
16
+ async function startSingleVm(name, options) {
17
+ const runningOnMac = isMacOS();
18
+ const needsSudo = runningOnMac && process.getuid && process.getuid() !== 0;
19
+ // If on macOS and needs bridged networking (default) but not running as root, re-run with sudo
20
+ if (needsSudo) {
21
+ console.log(chalk_1.default.yellow('Bridged networking on macOS requires root privileges. Attempting to re-run with sudo...'));
22
+ // Get the original arguments passed to the phy command
23
+ // process.argv is typically [node executable, script path, ...args]
24
+ // We want to run 'sudo phy ...args'
25
+ const originalArgs = process.argv.slice(2); // e.g., ['vm', 'start', 'myvm', '--headless']
26
+ const phyCommand = process.argv[1]; // Path to the phy script being executed
27
+ const sudoArgs = [phyCommand, ...originalArgs]; // Explicitly typed array
28
+ try {
29
+ // Explicitly use the string 'sudo' as the command
30
+ const sudoProcess = (0, child_process_1.spawn)('sudo', sudoArgs, { stdio: 'inherit' });
31
+ sudoProcess.on('error', (err) => {
32
+ console.error(chalk_1.default.red(`Failed to execute sudo: ${err.message}`));
33
+ console.error(chalk_1.default.red('Please ensure \'sudo\' is available and try running the command manually with sudo:'));
34
+ console.error(chalk_1.default.cyan(` sudo phy ${originalArgs.join(' ')}`));
35
+ process.exit(1);
36
+ });
37
+ // Wait for the sudo process to exit and exit this script with the same code
38
+ sudoProcess.on('close', (code) => {
39
+ process.exit(code === null ? 1 : code);
40
+ });
41
+ // Prevent the non-sudo version from continuing
42
+ return;
43
+ }
44
+ catch (error) {
45
+ console.error(chalk_1.default.red(`Error attempting to spawn sudo: ${error.message}`));
46
+ process.exit(1);
47
+ }
48
+ }
49
+ // Check if VM exists (do this *after* potential sudo relaunch)
50
+ if (!(0, vm_1.vmExists)(name)) {
51
+ throw new Error(`VM "${name}" not found`);
52
+ }
53
+ // Check if VM is already running
54
+ if ((0, vm_1.isVmRunning)(name)) {
55
+ console.log(chalk_1.default.yellow(`VM "${name}" is already running`));
56
+ return;
57
+ }
58
+ // Read VM configuration and settings
59
+ const config = (0, vm_1.readVmConfig)(name);
60
+ const settings = (0, vm_1.readVmSettings)(name);
61
+ const vmDir = (0, vm_1.getVmDir)(name);
62
+ // Determine memory to use - priority: command option > settings.json > config.json
63
+ const memory = options.memory
64
+ ? parseInt(options.memory, 10)
65
+ : settings.memory
66
+ ? settings.memory
67
+ : config.memory || 2048;
68
+ // Get host architecture to determine emulation status
69
+ const hostArch = getHostArch();
70
+ const isEmulated = config.arch !== hostArch;
71
+ // Force headless mode for cross-architecture emulation due to compatibility issues
72
+ let headless = options.headless !== undefined
73
+ ? options.headless
74
+ : settings.headless || false;
75
+ // Force headless mode for cross-architecture emulation (both directions)
76
+ if (isEmulated && ((config.arch === 'amd64' && hostArch === 'arm64') ||
77
+ (config.arch === 'arm64' && hostArch === 'amd64'))) {
78
+ const wasHeadless = headless;
79
+ headless = true; // Force headless mode
80
+ // If user explicitly requested non-headless mode, inform them
81
+ if (options.headless === false || (!options.headless && !settings.headless)) {
82
+ console.log(chalk_1.default.yellow(`⚠️ Running ${config.arch} VM on ${hostArch} architecture requires headless mode`));
83
+ console.log(chalk_1.default.yellow(` GUI mode has been disabled and switched to headless operation`));
84
+ console.log(chalk_1.default.blue(` Use 'phy vm shell ${name}' to access the console`));
85
+ }
86
+ }
87
+ // Build paths to image and disk files
88
+ const imageFile = path_1.default.join(vmDir, config.image);
89
+ const diskFile = path_1.default.join(vmDir, config.disk);
90
+ // Construct initramfs path if the filename exists in config
91
+ const initramfsFile = config.initramfs
92
+ ? path_1.default.join(vmDir, config.initramfs)
93
+ : undefined;
94
+ // Check if image and disk files exist
95
+ if (!fs_1.default.existsSync(imageFile) || !fs_1.default.existsSync(diskFile)) {
96
+ throw new Error(`VM image or disk file not found for "${name}"`);
97
+ }
98
+ // Also check initramfs file if it exists in config
99
+ if (config.initramfs && !fs_1.default.existsSync(initramfsFile)) {
100
+ throw new Error(`VM initramfs file not found for "${name}": ${initramfsFile}`);
101
+ }
102
+ // Get serial number from settings.json or serial.txt for backward compatibility
103
+ let serialNumber = settings.serial;
104
+ // If serial not in settings, try reading from serial.txt (backward compatibility)
105
+ if (!serialNumber) {
106
+ const serialPath = (0, vm_1.getVmSerialPath)(name);
107
+ if (fs_1.default.existsSync(serialPath)) {
108
+ serialNumber = fs_1.default.readFileSync(serialPath, 'utf8').trim();
109
+ }
110
+ }
111
+ if (serialNumber) {
112
+ console.log(chalk_1.default.blue(`Using serial number: ${serialNumber}`));
113
+ }
114
+ else {
115
+ console.log(chalk_1.default.yellow(`No serial number found for VM "${name}"`));
116
+ }
117
+ // Determine QEMU binary based on architecture
118
+ const qemuBin = config.arch === 'amd64'
119
+ ? 'qemu-system-x86_64'
120
+ : 'qemu-system-aarch64';
121
+ // Check if QEMU is installed first
122
+ try {
123
+ await checkQemuInstalled(qemuBin);
124
+ }
125
+ catch (error) {
126
+ // Attempt to install QEMU automatically
127
+ const installed = await attemptQemuInstall(qemuBin);
128
+ if (!installed) {
129
+ throw new Error(`QEMU binary '${qemuBin}' not found. Please install QEMU: ${error.message}`);
130
+ }
131
+ }
132
+ // Build QEMU command arguments - must exactly match the bash script format
133
+ const args = buildExactQemuArgs(config.arch, imageFile, diskFile, initramfsFile, memory, serialNumber, headless, null);
134
+ console.log(chalk_1.default.blue(`Starting PhyOS VM "${name}"...`));
135
+ console.log(chalk_1.default.blue(`Memory allocation: ${memory}MB`));
136
+ if (isEmulated) {
137
+ console.log(chalk_1.default.yellow(`Running in cross-architecture emulation mode: ${config.arch} VM on ${hostArch} host`));
138
+ if (process.platform === 'darwin') {
139
+ console.log(chalk_1.default.yellow(`Note: Cross-architecture emulation on macOS can be significantly slower than native.`));
140
+ if (config.arch === 'amd64') {
141
+ console.log(chalk_1.default.yellow(`Tip: On Apple Silicon Macs, install Rosetta 2 for better x86 emulation performance.`));
142
+ }
143
+ }
144
+ else {
145
+ console.log(chalk_1.default.yellow(`Note: Performance may be reduced in emulation mode`));
146
+ }
147
+ }
148
+ else {
149
+ console.log(chalk_1.default.green(`Running with native architecture: ${config.arch}`));
150
+ }
151
+ // Log the exact command being run
152
+ console.log(chalk_1.default.gray(`Running: ${qemuBin} ${args.join(' ')}`));
153
+ // Show headless status
154
+ if (headless) {
155
+ console.log(chalk_1.default.blue(`Running in headless mode - no GUI display`));
156
+ }
157
+ else {
158
+ console.log(chalk_1.default.blue(`Running with GUI display`));
159
+ }
160
+ // Open log file for writing
161
+ const logPath = (0, vm_1.getVmLogPath)(name);
162
+ const timestamp = new Date().toISOString();
163
+ const logFile = fs_1.default.openSync(logPath, 'a');
164
+ // Add startup timestamp to log
165
+ fs_1.default.writeSync(logFile, `\n\n========== Starting VM at ${timestamp} ==========\n\n`);
166
+ fs_1.default.writeSync(logFile, `Command: ${qemuBin} ${args.join(' ')}\n\n`);
167
+ // Create temporary file to capture error output
168
+ const errorLogPath = path_1.default.join((0, vm_1.getVmDir)(name), 'error.log');
169
+ const errorLogFile = fs_1.default.openSync(errorLogPath, 'w');
170
+ // Spawn QEMU process in detached mode, but capture stderr to help with debugging
171
+ const qemuProcess = (0, child_process_1.spawn)(qemuBin, args, {
172
+ detached: true,
173
+ stdio: ['ignore', logFile, errorLogFile]
174
+ });
175
+ // Handle process startup errors
176
+ qemuProcess.on('error', (err) => {
177
+ fs_1.default.writeSync(errorLogFile, `Process startup error: ${err.message}\n`);
178
+ console.error(chalk_1.default.red(`Failed to start QEMU: ${err.message}`));
179
+ throw err;
180
+ });
181
+ // Wait briefly to see if process starts successfully
182
+ await new Promise(resolve => setTimeout(resolve, 500));
183
+ // Check if process is still running
184
+ const pid = qemuProcess.pid;
185
+ if (!pid) {
186
+ // Read any error output to diagnose the issue
187
+ let errorOutput = '';
188
+ try {
189
+ fs_1.default.closeSync(errorLogFile);
190
+ if (fs_1.default.existsSync(errorLogPath)) {
191
+ errorOutput = fs_1.default.readFileSync(errorLogPath, 'utf8');
192
+ }
193
+ }
194
+ catch (e) {
195
+ // Ignore errors reading the error log
196
+ }
197
+ if (errorOutput) {
198
+ console.error(chalk_1.default.red(`QEMU Error Output: ${errorOutput}`));
199
+ throw new Error(`Failed to start VM process. QEMU Error: ${errorOutput}`);
200
+ }
201
+ else {
202
+ throw new Error('Failed to start VM process - no PID returned. Check if QEMU is properly installed.');
203
+ }
204
+ }
205
+ // Save PID to file atomically
206
+ const pidPath = (0, vm_1.getVmPidPath)(name);
207
+ const tempPidPath = `${pidPath}.tmp`;
208
+ try {
209
+ // Write to a temporary file first
210
+ fs_1.default.writeFileSync(tempPidPath, String(pid));
211
+ // Verify the temp file was actually created
212
+ if (!fs_1.default.existsSync(tempPidPath)) {
213
+ throw new Error('Failed to create temporary PID file');
214
+ }
215
+ // Ensure it's written to disk by syncing the file descriptor
216
+ const tempFd = fs_1.default.openSync(tempPidPath, 'r');
217
+ fs_1.default.fsyncSync(tempFd);
218
+ fs_1.default.closeSync(tempFd);
219
+ // Atomically rename the temporary file to the final file
220
+ fs_1.default.renameSync(tempPidPath, pidPath);
221
+ }
222
+ catch (error) {
223
+ // Clean up temp file if it exists
224
+ try {
225
+ if (fs_1.default.existsSync(tempPidPath)) {
226
+ fs_1.default.unlinkSync(tempPidPath);
227
+ }
228
+ }
229
+ catch (e) {
230
+ // Ignore cleanup errors
231
+ }
232
+ // If the PID file already exists (race condition), check if the process is actually running
233
+ if (fs_1.default.existsSync(pidPath)) {
234
+ try {
235
+ const existingPid = parseInt(fs_1.default.readFileSync(pidPath, 'utf8').trim(), 10);
236
+ // Check if the process is actually running
237
+ process.kill(existingPid, 0);
238
+ // If we get here, the process is running, so this is a legitimate "already running" case
239
+ console.log(chalk_1.default.yellow(`VM "${name}" is already running (PID: ${existingPid})`));
240
+ return;
241
+ }
242
+ catch (pidError) {
243
+ // Process is not running, remove stale PID file and retry
244
+ try {
245
+ fs_1.default.unlinkSync(pidPath);
246
+ // Retry writing the PID file once
247
+ fs_1.default.writeFileSync(tempPidPath, String(pid));
248
+ // Verify the temp file was created
249
+ if (!fs_1.default.existsSync(tempPidPath)) {
250
+ throw new Error('Failed to create temporary PID file on retry');
251
+ }
252
+ const tempFd = fs_1.default.openSync(tempPidPath, 'r');
253
+ fs_1.default.fsyncSync(tempFd);
254
+ fs_1.default.closeSync(tempFd);
255
+ fs_1.default.renameSync(tempPidPath, pidPath);
256
+ }
257
+ catch (retryError) {
258
+ // If we still can't write the PID file, but the process is running,
259
+ // we can still consider it a success
260
+ try {
261
+ process.kill(pid, 0);
262
+ console.log(chalk_1.default.yellow(`VM "${name}" started successfully but PID file could not be written`));
263
+ console.log(chalk_1.default.yellow(`Process is running with PID: ${pid}`));
264
+ return;
265
+ }
266
+ catch (killError) {
267
+ throw new Error(`Failed to write PID file after cleanup: ${retryError.message}`);
268
+ }
269
+ }
270
+ }
271
+ }
272
+ else {
273
+ // If we can't write the PID file but the process is running, consider it a success
274
+ try {
275
+ process.kill(pid, 0);
276
+ console.log(chalk_1.default.yellow(`VM "${name}" started successfully but PID file could not be written`));
277
+ console.log(chalk_1.default.yellow(`Process is running with PID: ${pid}`));
278
+ return;
279
+ }
280
+ catch (killError) {
281
+ throw new Error(`Failed to write PID file: ${error.message}`);
282
+ }
283
+ }
284
+ }
285
+ // Now that we have the PID, generate the real args with proper socket paths
286
+ const finalArgs = buildExactQemuArgs(config.arch, imageFile, diskFile, initramfsFile, memory, serialNumber, headless, pid);
287
+ // Log the actual command that should be used for debugging
288
+ const actualCommand = `${qemuBin} ${finalArgs.join(' ')}`;
289
+ fs_1.default.writeSync(logFile, `Actual command (with PID): ${actualCommand}\n\n`);
290
+ // Create socket paths with actual PID (not placeholder) and VM name
291
+ // Ensure pid is a valid value before using it
292
+ const safePid = pid || 'temp';
293
+ const vmName = path_1.default.basename(path_1.default.dirname(imageFile || '.'));
294
+ const monitorSocketPath = pid ? `/tmp/qemu-monitor-${vmName}-${safePid}.sock` : `/tmp/qemu-monitor-${vmName}-temp.sock`;
295
+ const serialSocketPath = pid ? `/tmp/qemu-serial-${vmName}-${safePid}.sock` : `/tmp/qemu-serial-${vmName}-temp.sock`;
296
+ // Unref the process so it can run in the background
297
+ qemuProcess.unref();
298
+ // Log the actual socket paths for debugging
299
+ fs_1.default.writeSync(logFile, `Monitor socket: ${monitorSocketPath}\n`);
300
+ fs_1.default.writeSync(logFile, `Serial socket: ${serialSocketPath}\n\n`);
301
+ // Log troubleshooting info
302
+ fs_1.default.writeSync(logFile, `To connect to this VM: phy vm shell ${name}\n`);
303
+ fs_1.default.writeSync(logFile, `Socket must exist at: ${serialSocketPath}\n\n`);
304
+ // Remove the existing dummy file creation that causes "Socket operation on non-socket" error
305
+ // QEMU will create the proper socket files when it starts up
306
+ try {
307
+ // If dummy socket files already exist from a previous run, delete them first
308
+ if (fs_1.default.existsSync(serialSocketPath)) {
309
+ fs_1.default.unlinkSync(serialSocketPath);
310
+ }
311
+ if (fs_1.default.existsSync(monitorSocketPath)) {
312
+ fs_1.default.unlinkSync(monitorSocketPath);
313
+ }
314
+ // Wait briefly for QEMU to start and potentially create the socket
315
+ console.log(chalk_1.default.blue(`Waiting for VM to initialize...`));
316
+ await new Promise(resolve => setTimeout(resolve, 2000));
317
+ }
318
+ catch (error) {
319
+ console.log(chalk_1.default.yellow(`Warning when cleaning up socket files: ${error.message}`));
320
+ }
321
+ console.log(chalk_1.default.green(`VM "${name}" started successfully`));
322
+ console.log(`To view logs: ${chalk_1.default.blue('phy vm logs')} ${name}`);
323
+ console.log(`To stop VM: ${chalk_1.default.blue('phy vm stop')} ${name}`);
324
+ // If headless, inform about shell access
325
+ if (headless) {
326
+ console.log(`To connect to console: ${chalk_1.default.blue('phy vm shell')} ${name}`);
327
+ }
328
+ }
329
+ /**
330
+ * Detects the host system architecture
331
+ */
332
+ function getHostArch() {
333
+ const systemArch = process.arch;
334
+ if (systemArch === 'x64')
335
+ return 'amd64';
336
+ if (systemArch === 'arm64')
337
+ return 'arm64';
338
+ return 'amd64'; // Default fallback
339
+ }
340
+ /**
341
+ * Detects if running on macOS
342
+ */
343
+ function isMacOS() {
344
+ return process.platform === 'darwin';
345
+ }
346
+ /**
347
+ * Gets the default network interface name on macOS
348
+ */
349
+ function getDefaultMacInterface() {
350
+ if (!isMacOS()) {
351
+ return null; // Only applicable on macOS
352
+ }
353
+ try {
354
+ // Run the command to get the default route
355
+ const output = (0, child_process_1.execSync)('route -n get default | grep interface:').toString();
356
+ // Parse the output (e.g., " interface: en8")
357
+ const match = output.match(/interface:\s*(\S+)/);
358
+ if (match && match[1]) {
359
+ console.log(chalk_1.default.blue(`Detected default network interface: ${match[1]}`));
360
+ return match[1];
361
+ }
362
+ console.log(chalk_1.default.yellow('Warning: Could not parse default interface from route command.'));
363
+ return null;
364
+ }
365
+ catch (error) {
366
+ console.log(chalk_1.default.yellow(`Warning: Failed to get default interface: ${error.message}`));
367
+ return null;
368
+ }
369
+ }
370
+ /**
371
+ * Builds QEMU command arguments to exactly match the original bash script
372
+ * Important: Each argument must be a separate array element without quotes
373
+ * Adds cross-architecture emulation support
374
+ */
375
+ function buildExactQemuArgs(arch, imageFile, diskFile, initramfsFile, memory, serialNumber, headless, pid) {
376
+ const hostArch = getHostArch();
377
+ const isEmulated = arch !== hostArch;
378
+ const runningOnMac = isMacOS();
379
+ // Get VM name from the image file path
380
+ const vmName = path_1.default.basename(path_1.default.dirname(imageFile || '.'));
381
+ // Create socket paths with actual PID (not placeholder) and VM name
382
+ // Ensure pid is a valid value before using it
383
+ const safePid = pid || 'temp';
384
+ const monitorSocketPath = pid ? `/tmp/qemu-monitor-${vmName}-${safePid}.sock` : `/tmp/qemu-monitor-${vmName}-temp.sock`;
385
+ const serialSocketPath = pid ? `/tmp/qemu-serial-${vmName}-${safePid}.sock` : `/tmp/qemu-serial-${vmName}-temp.sock`;
386
+ // Ensure memory is valid and convert to string
387
+ const memoryStr = memory ? String(memory) : '2048';
388
+ // Function to generate network arguments based on platform
389
+ const getNetworkArgs = () => {
390
+ if (runningOnMac) {
391
+ const defaultInterface = getDefaultMacInterface();
392
+ if (defaultInterface) {
393
+ console.log(chalk_1.default.blue(`Using vmnet-bridged with interface: ${defaultInterface}`));
394
+ return [
395
+ '-netdev', `vmnet-bridged,id=net0,ifname=${defaultInterface}`,
396
+ '-device', 'virtio-net-pci,netdev=net0'
397
+ ];
398
+ }
399
+ else {
400
+ console.log(chalk_1.default.yellow('Warning: Falling back to default vmnet-bridged mode without specific interface.'));
401
+ return [
402
+ '-netdev', 'vmnet-bridged,id=net0',
403
+ '-device', 'virtio-net-pci,netdev=net0'
404
+ ];
405
+ }
406
+ }
407
+ else {
408
+ // Use user-mode networking on other platforms
409
+ return [
410
+ '-netdev', 'user,id=net0',
411
+ '-device', 'virtio-net-pci,netdev=net0'
412
+ ];
413
+ }
414
+ };
415
+ if (arch === 'amd64') {
416
+ // x86_64 architecture
417
+ const args = [
418
+ '-machine', 'q35',
419
+ '-cpu', isEmulated ? 'max' : 'host',
420
+ '-m', memoryStr,
421
+ '-smp', '4',
422
+ '-kernel', imageFile,
423
+ '-drive', `if=none,file=${diskFile},format=raw,id=hd0`,
424
+ '-device', 'virtio-blk-pci,drive=hd0',
425
+ ...getNetworkArgs(), // Add network args
426
+ '-append', 'root=/dev/vda rw console=ttyS0'
427
+ ];
428
+ // Add KVM acceleration if running on Ubuntu/Debian Linux
429
+ if (process.platform === 'linux') {
430
+ // Check if running on Ubuntu/Debian by looking for common files
431
+ const isUbuntuDebian = fs_1.default.existsSync('/etc/debian_version') ||
432
+ fs_1.default.existsSync('/etc/lsb-release') ||
433
+ process.env.DISTRIB_ID === 'Ubuntu';
434
+ if (isUbuntuDebian) {
435
+ args.push('-enable-kvm');
436
+ }
437
+ }
438
+ // Add initramfs if available
439
+ if (initramfsFile) {
440
+ args.push('-initrd', initramfsFile);
441
+ }
442
+ // Add serial number if available
443
+ if (serialNumber) {
444
+ args.push('-smbios', `type=1,serial=${serialNumber}`);
445
+ }
446
+ // Add monitor and serial sockets with actual paths
447
+ args.push('-monitor', `unix:${monitorSocketPath},server,nowait`);
448
+ args.push('-serial', `unix:${serialSocketPath},server,nowait`);
449
+ if (headless) {
450
+ args.push('-display', 'none', '-nographic');
451
+ }
452
+ else {
453
+ // Use appropriate display for the platform
454
+ if (isEmulated) {
455
+ if (runningOnMac) {
456
+ // For macOS, use cocoa display
457
+ args.push('-display', 'cocoa');
458
+ }
459
+ else {
460
+ // For Linux and other platforms, try SDL
461
+ args.push('-display', 'sdl');
462
+ }
463
+ }
464
+ else {
465
+ args.push('-display', 'sdl', '-device', 'virtio-gpu-pci');
466
+ }
467
+ }
468
+ // Always add device controllers
469
+ args.push('-device', 'qemu-xhci', '-device', 'usb-tablet', '-device', 'usb-kbd');
470
+ return args;
471
+ }
472
+ else { // arm64
473
+ // Build kernel command line arguments
474
+ let kernelAppend = 'root=/dev/vda rw console=ttyAMA0';
475
+ if (serialNumber) {
476
+ kernelAppend += ` phygrid.serial=${serialNumber}`;
477
+ }
478
+ const args = [
479
+ '-machine', isEmulated ? 'virt' : 'virt,accel=hvf,highmem=off',
480
+ '-cpu', isEmulated ? 'cortex-a72' : 'host',
481
+ '-m', memoryStr,
482
+ '-smp', '4',
483
+ '-kernel', imageFile,
484
+ '-initrd', initramfsFile,
485
+ '-drive', `if=none,file=${diskFile},format=raw,id=hd0`,
486
+ '-device', 'virtio-blk-pci,drive=hd0',
487
+ ...getNetworkArgs(), // Add network args
488
+ '-append', kernelAppend
489
+ ];
490
+ // For ARM64 virt machines, SMBIOS is not fully supported, so we pass the serial via kernel cmdline
491
+ // Keep SMBIOS as fallback in case future QEMU versions support it better
492
+ if (serialNumber) {
493
+ args.push('-smbios', `type=1,serial=${serialNumber}`);
494
+ }
495
+ // Add monitor and serial sockets with actual paths
496
+ args.push('-monitor', `unix:${monitorSocketPath},server,nowait`);
497
+ args.push('-serial', `unix:${serialSocketPath},server,nowait`);
498
+ if (headless) {
499
+ args.push('-display', 'none', '-nographic');
500
+ }
501
+ else {
502
+ // Use appropriate display for the platform and emulation mode
503
+ if (isEmulated) {
504
+ if (runningOnMac) {
505
+ // For macOS emulating ARM, use cocoa display
506
+ args.push('-display', 'cocoa', '-device', 'virtio-gpu-pci');
507
+ }
508
+ else {
509
+ // For Linux and other platforms
510
+ args.push('-display', 'sdl', '-device', 'virtio-gpu-pci');
511
+ }
512
+ }
513
+ else {
514
+ args.push('-device', 'virtio-gpu-gl-pci', '-display', 'cocoa,gl=es', '-device', 'virtio-gpu-pci');
515
+ }
516
+ }
517
+ args.push('-device', 'qemu-xhci', '-device', 'usb-tablet', '-device', 'usb-kbd');
518
+ return args;
519
+ }
520
+ }
521
+ /**
522
+ * Check if QEMU is installed and available, prioritizing phygrid/qemu-virgl on macOS aarch64
523
+ */
524
+ async function checkQemuInstalled(qemuBin) {
525
+ if (qemuBin === 'qemu-system-aarch64' && isMacOS()) {
526
+ // macOS aarch64: Prioritize phygrid/qemu-virgl/qemu-virgl
527
+ try {
528
+ // Use the new helper for brew commands
529
+ (0, vm_1.runBrewCommandAsUser)(['ls', '--versions', 'phygrid/qemu-virgl/qemu-virgl']);
530
+ console.log(chalk_1.default.green('Found installed phygrid/qemu-virgl/qemu-virgl.'));
531
+ return true;
532
+ }
533
+ catch (phygridError) {
534
+ // phygrid/qemu-virgl/qemu-virgl is NOT installed. Check for standard qemu conflict.
535
+ try {
536
+ // Use the new helper for brew commands
537
+ (0, vm_1.runBrewCommandAsUser)(['ls', '--versions', 'qemu']);
538
+ // Standard qemu IS installed. Does it provide the binary?
539
+ try {
540
+ await execPromise(`which ${qemuBin}`); // Keep execPromise for non-brew 'which'
541
+ // Standard qemu is installed AND provides the binary - CONFLICT!
542
+ throw new Error(`Conflict: Standard 'qemu' package provides ${qemuBin}. Please uninstall it ('brew uninstall qemu') and install the required 'phygrid/qemu-virgl/qemu-virgl' formula.`);
543
+ }
544
+ catch (whichError) {
545
+ // Standard qemu installed, but doesn't provide the binary (unexpected?) or which failed.
546
+ }
547
+ }
548
+ catch (standardQemuError) {
549
+ // Standard qemu is also not installed. Good.
550
+ }
551
+ }
552
+ }
553
+ // Standard check using 'which' for non-macOS-aarch64 or as a fallback
554
+ return new Promise((resolve, reject) => {
555
+ (0, child_process_1.exec)(`which ${qemuBin}`, (error, stdout, stderr) => {
556
+ if (error) {
557
+ let message = `${qemuBin} is not installed or not in PATH.`;
558
+ if (qemuBin === 'qemu-system-aarch64' && isMacOS()) {
559
+ message += ` Ensure 'phygrid/qemu-virgl/qemu-virgl' is installed via Homebrew.`;
560
+ }
561
+ reject(new Error(message));
562
+ return;
563
+ }
564
+ if (!(qemuBin === 'qemu-system-aarch64' && isMacOS())) {
565
+ console.log(chalk_1.default.green(`Found ${qemuBin} at: ${stdout.trim()}`));
566
+ }
567
+ resolve(true);
568
+ });
569
+ });
570
+ }
571
+ /**
572
+ * Attempts to install QEMU based on the current OS
573
+ */
574
+ async function attemptQemuInstall(qemuBin) {
575
+ const platform = process.platform;
576
+ let targetPackageName;
577
+ let isPhygridFormula = false;
578
+ let manualInstallInstructions = `Please install ${qemuBin} manually.`;
579
+ if (platform === 'linux') {
580
+ // Determine Linux package name and manual instructions
581
+ try {
582
+ await execPromise('which apt-get');
583
+ if (qemuBin === 'qemu-system-x86_64') {
584
+ targetPackageName = 'qemu-system-x86';
585
+ }
586
+ else {
587
+ targetPackageName = 'qemu-system-arm qemu-efi-aarch64 qemu-system-aarch64';
588
+ }
589
+ manualInstallInstructions = `sudo apt-get update && sudo apt-get install -y ${targetPackageName.split(' ').join(' ')}`;
590
+ }
591
+ catch (e) {
592
+ try {
593
+ await execPromise('which yum');
594
+ if (qemuBin === 'qemu-system-x86_64') {
595
+ targetPackageName = 'qemu-system-x86';
596
+ }
597
+ else {
598
+ targetPackageName = 'qemu-system-aarch64';
599
+ }
600
+ manualInstallInstructions = `sudo yum install -y ${targetPackageName}`;
601
+ }
602
+ catch (e2) {
603
+ try {
604
+ await execPromise('which pacman');
605
+ targetPackageName = 'qemu';
606
+ manualInstallInstructions = `sudo pacman -S ${targetPackageName}`;
607
+ }
608
+ catch (e3) {
609
+ console.log(chalk_1.default.yellow('Could not detect Linux package manager for automatic installation'));
610
+ return false;
611
+ }
612
+ }
613
+ }
614
+ }
615
+ else if (platform === 'darwin') {
616
+ // Determine macOS package name and handle potential conflicts
617
+ try {
618
+ await execPromise('which brew');
619
+ if (qemuBin === 'qemu-system-aarch64') {
620
+ targetPackageName = 'phygrid/qemu-virgl/qemu-virgl';
621
+ isPhygridFormula = true;
622
+ manualInstallInstructions = `1. brew tap phygrid/qemu-virgl\n2. brew install ${targetPackageName}`;
623
+ // Check for conflicting standard qemu
624
+ try {
625
+ (0, vm_1.runBrewCommandAsUser)(['ls', '--versions', 'qemu']);
626
+ console.log(chalk_1.default.yellow("Warning: Found standard 'qemu' package installed, which may conflict."));
627
+ const { confirmUninstall } = await inquirer_1.default.prompt([{
628
+ type: 'confirm',
629
+ name: 'confirmUninstall',
630
+ message: `To install ${targetPackageName}, the standard 'qemu' package must be uninstalled first. Uninstall 'qemu' now?`,
631
+ default: false
632
+ }]);
633
+ if (confirmUninstall) {
634
+ console.log(chalk_1.default.blue('Uninstalling standard qemu (as user)...'));
635
+ (0, vm_1.runBrewCommandAsUser)(['uninstall', 'qemu']);
636
+ console.log(chalk_1.default.green('Standard qemu uninstalled.'));
637
+ }
638
+ else {
639
+ console.log(chalk_1.default.red("Installation aborted. Please manually uninstall 'qemu'."));
640
+ return false;
641
+ }
642
+ }
643
+ catch (e) { /* Standard qemu not found, ignore */ }
644
+ }
645
+ else { // x86_64 on macOS
646
+ targetPackageName = 'qemu';
647
+ manualInstallInstructions = `brew install ${targetPackageName}`;
648
+ }
649
+ }
650
+ catch (e) {
651
+ // Handle brew check/execution errors
652
+ if (e.message && e.message.includes('Failed to run brew command')) {
653
+ console.error(chalk_1.default.red(`Brew command failed: ${e.message}`));
654
+ }
655
+ else {
656
+ console.log(chalk_1.default.yellow('Homebrew (brew) command not found or failed. Ensure it is installed and in PATH.'));
657
+ console.log(chalk_1.default.blue('Visit https://brew.sh'));
658
+ }
659
+ return false;
660
+ }
661
+ }
662
+ else {
663
+ console.log(chalk_1.default.yellow(`Automatic installation not supported on ${platform}.`));
664
+ return false;
665
+ }
666
+ // Ensure we determined a package name
667
+ if (!targetPackageName) {
668
+ console.log(chalk_1.default.red('Internal error: Could not determine package name for installation.'));
669
+ return false;
670
+ }
671
+ // Confirm installation with user
672
+ const { confirmInstall } = await inquirer_1.default.prompt([{
673
+ type: 'confirm',
674
+ name: 'confirmInstall',
675
+ message: `${targetPackageName} is required but not installed. Install now?`,
676
+ default: true
677
+ }]);
678
+ if (!confirmInstall) {
679
+ console.log(chalk_1.default.yellow('Installation cancelled by user.'));
680
+ console.log(chalk_1.default.blue(`Manual instructions: ${manualInstallInstructions}`));
681
+ return false;
682
+ }
683
+ // Perform the installation
684
+ console.log(chalk_1.default.blue(`Attempting to install ${targetPackageName}...`));
685
+ try {
686
+ if (platform === 'darwin') {
687
+ // macOS installation (using helper)
688
+ if (isPhygridFormula) {
689
+ console.log(chalk_1.default.gray('Running: brew tap phygrid/qemu-virgl (as user)'));
690
+ (0, vm_1.runBrewCommandAsUser)(['tap', 'phygrid/qemu-virgl']);
691
+ }
692
+ console.log(chalk_1.default.gray(`Running: brew install ${targetPackageName} (as user)`));
693
+ (0, vm_1.runBrewCommandAsUser)(['install', targetPackageName]);
694
+ }
695
+ else {
696
+ // Linux installation (requires sudo directly)
697
+ console.log(chalk_1.default.gray(`Running: ${manualInstallInstructions}`));
698
+ await new Promise((resolve, reject) => {
699
+ // Execute the command string which includes sudo
700
+ const childProcess = (0, child_process_1.spawn)(manualInstallInstructions, { shell: true, stdio: 'inherit' });
701
+ childProcess.on('close', (code) => code === 0 ? resolve(true) : reject(new Error(`Installation failed with code ${code}`)));
702
+ childProcess.on('error', reject);
703
+ });
704
+ }
705
+ console.log(chalk_1.default.green(`Successfully initiated installation for ${targetPackageName}`));
706
+ // Verify installation (may take a moment for PATH to update)
707
+ console.log(chalk_1.default.blue('Verifying installation...'));
708
+ await new Promise(resolve => setTimeout(resolve, 1500)); // Brief pause
709
+ try {
710
+ await checkQemuInstalled(qemuBin);
711
+ console.log(chalk_1.default.green('Verification successful.'));
712
+ return true;
713
+ }
714
+ catch (e) {
715
+ console.log(chalk_1.default.red(`Installation done, but ${qemuBin} verification failed.`));
716
+ console.log(chalk_1.default.yellow(e.message || 'Unknown verification error.'));
717
+ console.log(chalk_1.default.yellow('Check PATH or try running the phy command again.'));
718
+ return false;
719
+ }
720
+ }
721
+ catch (error) {
722
+ console.log(chalk_1.default.red(`Failed to install ${targetPackageName}: ${error.message}`));
723
+ console.log(chalk_1.default.yellow('Please try the manual installation steps:'));
724
+ console.log(chalk_1.default.blue(manualInstallInstructions));
725
+ return false;
726
+ }
727
+ }
728
+ /**
729
+ * Promise-based wrapper for exec
730
+ */
731
+ function execPromise(command) {
732
+ return new Promise((resolve, reject) => {
733
+ (0, child_process_1.exec)(command, (error, stdout, stderr) => {
734
+ if (error) {
735
+ // Log stderr for context if available
736
+ if (stderr) {
737
+ // console.error(`Command failed: ${command}\nstderr: ${stderr.trim()}`);
738
+ }
739
+ reject(error);
740
+ return;
741
+ }
742
+ resolve(stdout.trim());
743
+ });
744
+ });
745
+ }
746
+ /**
747
+ * Starts one or more PhyOS VMs
748
+ */
749
+ async function startVm(names, options) {
750
+ // Handle single name (for backward compatibility)
751
+ const vmNames = Array.isArray(names) ? names : [names];
752
+ if (vmNames.length === 0) {
753
+ throw new Error('At least one VM name is required');
754
+ }
755
+ // Check which VMs exist and which don't
756
+ const existingVms = [];
757
+ const missingVms = [];
758
+ const alreadyRunningVms = [];
759
+ vmNames.forEach(name => {
760
+ if (!(0, vm_1.vmExists)(name)) {
761
+ missingVms.push(name);
762
+ }
763
+ else if ((0, vm_1.isVmRunning)(name)) {
764
+ alreadyRunningVms.push(name);
765
+ }
766
+ else {
767
+ existingVms.push(name);
768
+ }
769
+ });
770
+ // Report status
771
+ if (missingVms.length > 0) {
772
+ console.error(chalk_1.default.red(`❗️ The following VMs were not found: ${missingVms.join(', ')}`));
773
+ }
774
+ if (alreadyRunningVms.length > 0) {
775
+ console.log(chalk_1.default.yellow(`⚠️ The following VMs are already running: ${alreadyRunningVms.join(', ')}`));
776
+ }
777
+ // If no VMs to start, exit
778
+ if (existingVms.length === 0) {
779
+ if (alreadyRunningVms.length > 0 && missingVms.length === 0) {
780
+ console.log(chalk_1.default.green('All specified VMs are already running'));
781
+ return;
782
+ }
783
+ throw new Error('No VMs found to start');
784
+ }
785
+ // Show what will be started
786
+ console.log(chalk_1.default.blue(`Starting ${existingVms.length} VM${existingVms.length > 1 ? 's' : ''}: ${existingVms.join(', ')}`));
787
+ // Start VMs sequentially to avoid resource conflicts
788
+ const startedVms = [];
789
+ const failedVms = [];
790
+ for (const name of existingVms) {
791
+ try {
792
+ console.log(chalk_1.default.blue(`\n[${startedVms.length + failedVms.length + alreadyRunningVms.length + 1}/${existingVms.length}] Starting VM: ${name}`));
793
+ // Double-check if VM is running before starting (in case it was started by another process)
794
+ if ((0, vm_1.isVmRunning)(name)) {
795
+ console.log(chalk_1.default.yellow(`VM "${name}" is already running`));
796
+ alreadyRunningVms.push(name);
797
+ continue;
798
+ }
799
+ await startSingleVm(name, options);
800
+ startedVms.push(name);
801
+ console.log(chalk_1.default.green(`✅ VM "${name}" started successfully`));
802
+ }
803
+ catch (error) {
804
+ // Check if the VM is actually running despite the error (race condition)
805
+ if ((0, vm_1.isVmRunning)(name)) {
806
+ console.log(chalk_1.default.yellow(`VM "${name}" is already running (detected after start attempt)`));
807
+ alreadyRunningVms.push(name);
808
+ }
809
+ else {
810
+ // Check if the process is running by trying to find it in the process list
811
+ // This handles cases where PID file operations failed but the VM is actually running
812
+ try {
813
+ const config = (0, vm_1.readVmConfig)(name);
814
+ const qemuBin = config.arch === 'amd64' ? 'qemu-system-x86_64' : 'qemu-system-aarch64';
815
+ // Use ps to check if the QEMU process is running for this VM
816
+ const psOutput = (0, child_process_1.execSync)(`ps aux | grep "${qemuBin}" | grep "${name}" | grep -v grep`, { encoding: 'utf8' });
817
+ if (psOutput.trim()) {
818
+ console.log(chalk_1.default.yellow(`VM "${name}" is running (detected via process list) but PID file is missing`));
819
+ console.log(chalk_1.default.yellow(`This may be due to permission issues with PID file creation`));
820
+ // Try to create the PID file manually
821
+ if ((0, vm_1.createPidFileForRunningVm)(name)) {
822
+ console.log(chalk_1.default.green(`✅ Successfully created PID file for VM "${name}"`));
823
+ }
824
+ else {
825
+ console.log(chalk_1.default.yellow(`⚠️ Could not create PID file for VM "${name}" - VM status may be inconsistent`));
826
+ }
827
+ alreadyRunningVms.push(name);
828
+ }
829
+ else {
830
+ console.error(chalk_1.default.red(`❗️ Failed to start VM "${name}": ${error.message}`));
831
+ failedVms.push({ name, error: error.message });
832
+ }
833
+ }
834
+ catch (psError) {
835
+ // If we can't check the process list, fall back to the original error
836
+ console.error(chalk_1.default.red(`❗️ Failed to start VM "${name}": ${error.message}`));
837
+ failedVms.push({ name, error: error.message });
838
+ }
839
+ }
840
+ }
841
+ }
842
+ // Summary
843
+ console.log(chalk_1.default.blue('\n📊 Summary:'));
844
+ if (startedVms.length > 0) {
845
+ console.log(chalk_1.default.green(`✅ Successfully started ${startedVms.length} VM${startedVms.length > 1 ? 's' : ''}: ${startedVms.join(', ')}`));
846
+ }
847
+ if (alreadyRunningVms.length > 0) {
848
+ console.log(chalk_1.default.yellow(`⚠️ Already running: ${alreadyRunningVms.join(', ')}`));
849
+ }
850
+ if (failedVms.length > 0) {
851
+ console.log(chalk_1.default.red(`❌ Failed to start ${failedVms.length} VM${failedVms.length > 1 ? 's' : ''}: ${failedVms.map(f => f.name).join(', ')}`));
852
+ }
853
+ if (missingVms.length > 0) {
854
+ console.log(chalk_1.default.yellow(`⚠️ VMs not found: ${missingVms.join(', ')}`));
855
+ }
856
+ // Throw error if any operations failed
857
+ if (failedVms.length > 0 || missingVms.length > 0) {
858
+ throw new Error(`Some VM start operations failed or VMs were not found`);
859
+ }
860
+ }
861
+ //# sourceMappingURL=start.js.map