@phystack/cli 4.4.29

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 +322 -0
  9. package/dist/commands/app/build-container.js.map +1 -0
  10. package/dist/commands/app/build.js +375 -0
  11. package/dist/commands/app/build.js.map +1 -0
  12. package/dist/commands/app/create.js +409 -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 +546 -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 +189 -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,1120 @@
1
+ "use strict";
2
+ /**
3
+ * # PhyOS Flash Tool
4
+ *
5
+ * This command allows users to easily flash PhyOS images to USB drives or SD cards.
6
+ * It works similarly to tools like Balena Etcher but via CLI.
7
+ *
8
+ * ## Features:
9
+ * - Downloads images from https://os.phygrid.com/ if not found in cache
10
+ * - Uses the same cache directory as VM commands (~/.config/phygrid-cli/cache/)
11
+ * - Detects removable drives on both macOS and Linux
12
+ * - Supports compressed images (gzip, xz)
13
+ * - Safety checks to avoid accidental flashing of system disks
14
+ * - Optional raw mode for advanced users who need to see all disks
15
+ *
16
+ * ## Usage:
17
+ * ```
18
+ * phy flash phyos.5.0.127.img.gz # Flash a gzipped image
19
+ * phy flash phyos.5.0.127.img.xz # Flash an xz compressed image
20
+ * phy flash phyos.5.0.127.img # Flash an uncompressed image
21
+ * phy flash phyos-latest.img.gz --raw # Show all disks including system disks
22
+ * ```
23
+ *
24
+ * ## Workflow:
25
+ * 1. Checks if the image exists in cache, downloads if needed
26
+ * 2. Lists available removable drives
27
+ * 3. Prompts user to select a drive
28
+ * 4. Confirms before flashing (requires typing 'yes')
29
+ * 5. Flashes the image with appropriate compression handling
30
+ * 6. Shows progress during flashing
31
+ *
32
+ * ## Supported Platforms:
33
+ * - macOS: Uses diskutil to list and manage drives
34
+ * - Linux: Uses lsblk to list drives and dd for flashing
35
+ */
36
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
37
+ if (k2 === undefined) k2 = k;
38
+ var desc = Object.getOwnPropertyDescriptor(m, k);
39
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
40
+ desc = { enumerable: true, get: function() { return m[k]; } };
41
+ }
42
+ Object.defineProperty(o, k2, desc);
43
+ }) : (function(o, m, k, k2) {
44
+ if (k2 === undefined) k2 = k;
45
+ o[k2] = m[k];
46
+ }));
47
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
48
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
49
+ }) : function(o, v) {
50
+ o["default"] = v;
51
+ });
52
+ var __importStar = (this && this.__importStar) || (function () {
53
+ var ownKeys = function(o) {
54
+ ownKeys = Object.getOwnPropertyNames || function (o) {
55
+ var ar = [];
56
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
57
+ return ar;
58
+ };
59
+ return ownKeys(o);
60
+ };
61
+ return function (mod) {
62
+ if (mod && mod.__esModule) return mod;
63
+ var result = {};
64
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
65
+ __setModuleDefault(result, mod);
66
+ return result;
67
+ };
68
+ })();
69
+ var __importDefault = (this && this.__importDefault) || function (mod) {
70
+ return (mod && mod.__esModule) ? mod : { "default": mod };
71
+ };
72
+ Object.defineProperty(exports, "__esModule", { value: true });
73
+ exports.flashImageToDevice = flashImageToDevice;
74
+ const commander_1 = require("commander");
75
+ const chalk_1 = __importDefault(require("chalk"));
76
+ const path_1 = __importDefault(require("path"));
77
+ const fs_1 = __importDefault(require("fs"));
78
+ const os_1 = __importDefault(require("os"));
79
+ const child_process_1 = require("child_process");
80
+ const vm_1 = require("../utils/vm");
81
+ const utils_1 = require("../utils");
82
+ // Main flash command function
83
+ async function flashImage(filename, options = {}) {
84
+ try {
85
+ // Import inquirer dynamically since it's a heavy dependency
86
+ const inquirer = (await Promise.resolve().then(() => __importStar(require('inquirer')))).default;
87
+ // Determine cache path
88
+ const cacheDir = (0, vm_1.getVmsCacheDir)();
89
+ // Ensure cache directory exists
90
+ if (!fs_1.default.existsSync(cacheDir)) {
91
+ fs_1.default.mkdirSync(cacheDir, { recursive: true });
92
+ }
93
+ // If no filename provided, prompt user to select architecture
94
+ if (!filename) {
95
+ console.log(chalk_1.default.blue("No image specified. Please select device architecture:"));
96
+ const { architecture } = await inquirer.prompt([
97
+ {
98
+ type: 'list',
99
+ name: 'architecture',
100
+ message: 'Select the architecture for your target device:',
101
+ choices: [
102
+ { name: 'Intel based systems (AMD64)', value: 'amd64' },
103
+ { name: 'Giada DN74 (ARM64)', value: 'arm64' }
104
+ ]
105
+ }
106
+ ]);
107
+ // Get the URL for the selected architecture
108
+ const url = architecture === 'amd64'
109
+ ? 'https://qr.run/phyos-bundle-amd64-latest'
110
+ : 'https://qr.run/phyos-bundle-arm64-latest';
111
+ console.log(chalk_1.default.blue(`Resolving ${architecture} image from ${url}...`));
112
+ // Get the actual filename from the redirect
113
+ filename = await resolveRedirectFilename(url);
114
+ console.log(chalk_1.default.blue(`Selected ${architecture} image: ${filename}`));
115
+ }
116
+ // Generate path for the cached file
117
+ const cachePath = path_1.default.join(cacheDir, filename);
118
+ // Check if file exists in cache or download it
119
+ if (!fs_1.default.existsSync(cachePath)) {
120
+ console.log(chalk_1.default.blue(`Image ${filename} not found in cache. Downloading...`));
121
+ // If it's one of our shortlink URLs, use the resolved URL
122
+ if (filename.startsWith('phyos-bundle-')) {
123
+ const url = filename.includes('amd64')
124
+ ? 'https://qr.run/phyos-bundle-amd64-latest'
125
+ : 'https://qr.run/phyos-bundle-arm64-latest';
126
+ await downloadImageFromUrl(url, cachePath);
127
+ }
128
+ else {
129
+ // Normal download from phygrid.com
130
+ await downloadImage(filename, cachePath);
131
+ }
132
+ }
133
+ else {
134
+ console.log(chalk_1.default.green(`Found cached image: ${filename}`));
135
+ }
136
+ // Verify file exists
137
+ if (!fs_1.default.existsSync(cachePath)) {
138
+ throw new Error(`Failed to download or find image: ${filename}`);
139
+ }
140
+ // Determine if this is a compressed file
141
+ const isGzipped = filename.endsWith('.gz');
142
+ const isXz = filename.endsWith('.xz');
143
+ // Inform about raw disk option if set
144
+ if (options.raw) {
145
+ console.log(chalk_1.default.yellow('Raw disk mode enabled. Use this only if you know what you are doing.'));
146
+ console.log(chalk_1.default.yellow('This mode will show all disks, including system disks that should NOT be flashed.'));
147
+ }
148
+ // Check for required tools and install if needed
149
+ console.log(chalk_1.default.blue('Checking required tools...'));
150
+ const toolsAvailable = await checkAndInstallRequiredTools();
151
+ if (!toolsAvailable) {
152
+ throw new Error('Required tools are missing. Please install them as instructed above.');
153
+ }
154
+ // Process for selecting and flashing drive
155
+ let selectedDrive = null;
156
+ let selectedDriveName = null;
157
+ let exitLoop = false;
158
+ while (!exitLoop) {
159
+ // Get list of removable drives
160
+ const drives = await listRemovableDrives(options.raw);
161
+ if (drives.length === 0) {
162
+ console.log(chalk_1.default.yellow('No removable drives detected.'));
163
+ const { retry } = await inquirer.prompt([
164
+ {
165
+ type: 'confirm',
166
+ name: 'retry',
167
+ message: 'Would you like to scan for drives again?',
168
+ default: true
169
+ }
170
+ ]);
171
+ if (!retry) {
172
+ exitLoop = true;
173
+ throw new Error('No drives available for flashing. Operation cancelled.');
174
+ }
175
+ continue;
176
+ }
177
+ // Add an option to rescan in the choices
178
+ const choices = [
179
+ {
180
+ name: 'Rescan for drives',
181
+ value: 'rescan'
182
+ },
183
+ ...drives.map(drive => ({
184
+ name: drive.displayName,
185
+ value: drive
186
+ })),
187
+ {
188
+ name: 'Cancel operation',
189
+ value: 'cancel'
190
+ }
191
+ ];
192
+ // Prompt user to select a drive
193
+ const { drive } = await inquirer.prompt([
194
+ {
195
+ type: 'list',
196
+ name: 'drive',
197
+ message: 'Select a drive to flash:',
198
+ choices
199
+ }
200
+ ]);
201
+ if (drive === 'rescan') {
202
+ console.log(chalk_1.default.blue('Rescanning for drives...'));
203
+ continue;
204
+ }
205
+ if (drive === 'cancel') {
206
+ exitLoop = true;
207
+ throw new Error('Operation cancelled by user.');
208
+ }
209
+ // Final confirmation before flashing
210
+ const { confirmation } = await inquirer.prompt([
211
+ {
212
+ type: 'input',
213
+ name: 'confirmation',
214
+ message: chalk_1.default.red(`WARNING: This will erase ALL data on ${drive.displayName}. Type 'yes' to confirm:`),
215
+ validate: (input) => {
216
+ if (input.toLowerCase() === 'yes')
217
+ return true;
218
+ return 'You must type \'yes\' to confirm';
219
+ }
220
+ }
221
+ ]);
222
+ if (confirmation.toLowerCase() === 'yes') {
223
+ selectedDrive = drive.path;
224
+ selectedDriveName = drive.displayName;
225
+ exitLoop = true;
226
+ }
227
+ }
228
+ if (!selectedDrive) {
229
+ throw new Error('No drive selected for flashing. Operation cancelled.');
230
+ }
231
+ // Ask if verification is desired
232
+ const { verify } = await inquirer.prompt([
233
+ {
234
+ type: 'confirm',
235
+ name: 'verify',
236
+ message: 'Would you like to verify the flashed image after writing? (recommended but takes longer)',
237
+ default: true
238
+ }
239
+ ]);
240
+ // Special handling for macOS - unmount disk before flashing
241
+ const platform = os_1.default.platform();
242
+ if (platform === 'darwin' && selectedDrive) {
243
+ try {
244
+ // Extract disk identifier (e.g., disk2 from /dev/disk2)
245
+ const diskName = path_1.default.basename(selectedDrive);
246
+ console.log(chalk_1.default.blue(`Unmounting disk ${diskName}...`));
247
+ (0, child_process_1.execSync)(`diskutil unmountDisk ${diskName}`, { stdio: 'inherit' });
248
+ }
249
+ catch (error) {
250
+ console.warn(chalk_1.default.yellow(`Warning: Failed to unmount disk: ${error.message}`));
251
+ console.warn(chalk_1.default.yellow(`Continuing anyway, but flashing may fail if the disk is mounted.`));
252
+ }
253
+ }
254
+ // Start flashing
255
+ console.log(chalk_1.default.blue(`\nPreparing to flash ${filename} to ${selectedDriveName} (${selectedDrive})...`));
256
+ console.log(chalk_1.default.yellow('This may take several minutes. Do not disconnect the drive.'));
257
+ // Flash image to device with new method - pass cache directory
258
+ await flashImageToDevice(cachePath, selectedDrive, isGzipped, isXz, verify, cacheDir);
259
+ console.log(chalk_1.default.green('\n✓ Operation completed successfully!'));
260
+ console.log(chalk_1.default.blue('It is now safe to eject the drive.'));
261
+ }
262
+ catch (error) {
263
+ console.error(chalk_1.default.red(`\n❗️Error: ${error.message}`));
264
+ throw error;
265
+ }
266
+ }
267
+ /**
268
+ * Download the image file from the server
269
+ */
270
+ async function downloadImage(filename, outputPath) {
271
+ return new Promise((resolve, reject) => {
272
+ // Use omborigrid.com for griddevice files, phygrid.com for others
273
+ const baseUrl = filename.startsWith('griddevice')
274
+ ? 'https://os.omborigrid.com'
275
+ : 'https://os.phygrid.com';
276
+ const url = `${baseUrl}/${filename}`;
277
+ console.log(chalk_1.default.blue(`Downloading from ${url}...`));
278
+ // Use curl for download with progress bar
279
+ const curl = (0, child_process_1.spawn)('curl', [
280
+ '--progress-bar', // Show progress bar
281
+ '-L', // Follow redirects
282
+ '-o', outputPath, // Output file
283
+ url // URL to download
284
+ ], {
285
+ stdio: ['ignore', 'inherit', 'inherit'] // Show progress in terminal
286
+ });
287
+ curl.on('close', (code) => {
288
+ if (code === 0) {
289
+ resolve();
290
+ }
291
+ else {
292
+ reject(new Error(`Download failed with code ${code}`));
293
+ }
294
+ });
295
+ curl.on('error', (err) => {
296
+ reject(new Error(`Failed to execute curl: ${err.message}`));
297
+ });
298
+ });
299
+ }
300
+ /**
301
+ * Download an image file from a complete URL
302
+ */
303
+ async function downloadImageFromUrl(url, outputPath) {
304
+ return new Promise((resolve, reject) => {
305
+ console.log(chalk_1.default.blue(`Downloading from ${url}...`));
306
+ // Use curl for download with progress bar
307
+ const curl = (0, child_process_1.spawn)('curl', [
308
+ '--progress-bar', // Show progress bar
309
+ '-L', // Follow redirects
310
+ '-o', outputPath, // Output file
311
+ url // URL to download
312
+ ], {
313
+ stdio: ['ignore', 'inherit', 'inherit'] // Show progress in terminal
314
+ });
315
+ curl.on('close', (code) => {
316
+ if (code === 0) {
317
+ resolve();
318
+ }
319
+ else {
320
+ reject(new Error(`Download failed with code ${code}`));
321
+ }
322
+ });
323
+ curl.on('error', (err) => {
324
+ reject(new Error(`Failed to execute curl: ${err.message}`));
325
+ });
326
+ });
327
+ }
328
+ /**
329
+ * Resolve a redirect URL to get the actual filename
330
+ */
331
+ async function resolveRedirectFilename(url) {
332
+ return new Promise((resolve, reject) => {
333
+ // Use curl to follow redirects and get the final URL
334
+ const curl = (0, child_process_1.spawn)('curl', [
335
+ '-s', // Silent mode
336
+ '-I', // Headers only
337
+ '-L', // Follow redirects
338
+ url // URL to check
339
+ ]);
340
+ let output = '';
341
+ curl.stdout.on('data', (data) => {
342
+ output += data.toString();
343
+ });
344
+ curl.on('close', (code) => {
345
+ if (code === 0) {
346
+ // Extract the filename from the Location header
347
+ const locationMatch = output.match(/location: (.+?)[\r\n]/i);
348
+ if (locationMatch && locationMatch[1]) {
349
+ const finalUrl = locationMatch[1].trim();
350
+ const filename = path_1.default.basename(finalUrl);
351
+ resolve(filename);
352
+ }
353
+ else {
354
+ reject(new Error('Could not resolve redirect URL'));
355
+ }
356
+ }
357
+ else {
358
+ reject(new Error(`Failed to follow redirect with code ${code}`));
359
+ }
360
+ });
361
+ curl.on('error', (err) => {
362
+ reject(new Error(`Failed to execute curl: ${err.message}`));
363
+ });
364
+ });
365
+ }
366
+ /**
367
+ * List removable drives on the system
368
+ */
369
+ async function listRemovableDrives(showAllDisks = false) {
370
+ const platform = os_1.default.platform();
371
+ if (platform === 'darwin') {
372
+ // macOS - use diskutil
373
+ try {
374
+ // Get internal and external disks based on option
375
+ const diskutilCmd = showAllDisks ? 'diskutil list' : 'diskutil list external';
376
+ const output = (0, child_process_1.execSync)(diskutilCmd, { encoding: 'utf8' });
377
+ // Find physical disk devices (not partitions)
378
+ const diskRegex = /\/dev\/(disk\d+)\s+\((external|internal),\s+physical\):/g;
379
+ let match;
380
+ const drives = [];
381
+ while ((match = diskRegex.exec(output)) !== null) {
382
+ const diskId = match[1]; // e.g., "disk4"
383
+ const diskType = match[2]; // "external" or "internal"
384
+ // Skip internal disks unless in raw mode
385
+ if (!showAllDisks && diskType === 'internal') {
386
+ console.log(chalk_1.default.yellow(`Skipping internal disk: /dev/${diskId}`));
387
+ continue;
388
+ }
389
+ // For system protection, skip the boot disk unless in raw mode
390
+ if (!showAllDisks) {
391
+ try {
392
+ const bootDisk = (0, child_process_1.execSync)('diskutil info /', { encoding: 'utf8' });
393
+ const bootDiskMatch = bootDisk.match(/Device Identifier:\s+([^\s]+)/);
394
+ if (bootDiskMatch && bootDiskMatch[1] === diskId) {
395
+ console.log(chalk_1.default.yellow(`Skipping boot disk: /dev/${diskId}`));
396
+ continue;
397
+ }
398
+ }
399
+ catch (error) {
400
+ // If we can't determine the boot disk, proceed anyway
401
+ }
402
+ }
403
+ // Get additional disk information
404
+ let vendor = '';
405
+ let model = '';
406
+ let size = '';
407
+ try {
408
+ // Get more detailed information about the disk
409
+ const diskInfo = (0, child_process_1.execSync)(`diskutil info ${diskId}`, { encoding: 'utf8' });
410
+ // Extract size
411
+ const sizeMatch = diskInfo.match(/Disk Size:\s+([^(]+)/);
412
+ if (sizeMatch && sizeMatch[1]) {
413
+ size = sizeMatch[1].trim();
414
+ }
415
+ // Extract model/vendor
416
+ const deviceModel = diskInfo.match(/Device Model:\s+(.+?)$/m);
417
+ if (deviceModel && deviceModel[1]) {
418
+ model = deviceModel[1].trim();
419
+ }
420
+ // Extract media name
421
+ const mediaName = diskInfo.match(/Media Name:\s+(.+?)$/m);
422
+ if (mediaName && mediaName[1]) {
423
+ vendor = mediaName[1].trim();
424
+ }
425
+ }
426
+ catch (error) {
427
+ // Fallback to basic information if detailed info not available
428
+ console.log(chalk_1.default.yellow(`Couldn't get detailed info for /dev/${diskId}: ${error.message}`));
429
+ }
430
+ // Prepare display name with detailed information
431
+ const displayDetails = [];
432
+ if (size)
433
+ displayDetails.push(size);
434
+ if (vendor)
435
+ displayDetails.push(vendor);
436
+ if (model && model !== vendor)
437
+ displayDetails.push(model);
438
+ // Add this disk to our list
439
+ drives.push({
440
+ path: `/dev/${diskId}`,
441
+ displayName: `/dev/${diskId} (${displayDetails.join(' • ')})`,
442
+ vendor,
443
+ model,
444
+ size
445
+ });
446
+ }
447
+ return drives;
448
+ }
449
+ catch (error) {
450
+ console.error(chalk_1.default.red(`Error listing drives: ${error.message}`));
451
+ return [];
452
+ }
453
+ }
454
+ else if (platform === 'linux') {
455
+ // Linux - use lsblk
456
+ try {
457
+ // Use lsblk with different options depending on whether we want all disks
458
+ const lsblkCmd = showAllDisks
459
+ ? 'lsblk -d -o NAME,SIZE,MODEL,VENDOR,SERIAL,TRAN --noheadings'
460
+ : 'lsblk -d -o NAME,SIZE,RM,MODEL,VENDOR,SERIAL,TRAN --noheadings';
461
+ const output = (0, child_process_1.execSync)(lsblkCmd, { encoding: 'utf8' });
462
+ const drives = [];
463
+ // Parse lsblk output
464
+ output.split('\n').forEach(line => {
465
+ const fields = line.trim().split(/\s+/);
466
+ if (fields.length < 2)
467
+ return; // Skip empty lines
468
+ const name = fields[0];
469
+ const size = fields[1];
470
+ // If we're only showing removable drives, check the RM flag
471
+ if (!showAllDisks) {
472
+ const isRemovable = fields.length >= 3 && fields[2] === '1';
473
+ if (!isRemovable)
474
+ return;
475
+ // Move to next position after RM flag
476
+ fields.splice(2, 1);
477
+ }
478
+ // Skip known system disks if not in raw mode
479
+ if (!showAllDisks && (name === 'sda' || name === 'nvme0n1')) {
480
+ try {
481
+ // Check if this is the boot disk
482
+ const mountInfo = (0, child_process_1.execSync)(`findmnt -n -o SOURCE /`, { encoding: 'utf8' });
483
+ if (mountInfo.includes(name)) {
484
+ console.log(chalk_1.default.yellow(`Skipping boot disk: /dev/${name}`));
485
+ return;
486
+ }
487
+ }
488
+ catch (error) {
489
+ // If we can't determine boot disk, still include this disk
490
+ }
491
+ }
492
+ // Extract additional information
493
+ const model = fields.length >= 3 ? fields[2] : '';
494
+ const vendor = fields.length >= 4 ? fields[3] : '';
495
+ const serial = fields.length >= 5 ? fields[4] : '';
496
+ const transport = fields.length >= 6 ? fields[5] : '';
497
+ // Prepare display name with rich information
498
+ const displayDetails = [];
499
+ if (size)
500
+ displayDetails.push(size);
501
+ if (vendor)
502
+ displayDetails.push(vendor);
503
+ if (model)
504
+ displayDetails.push(model);
505
+ if (transport)
506
+ displayDetails.push(transport);
507
+ if (serial && serial !== '0')
508
+ displayDetails.push(`S/N: ${serial.substring(0, 8)}...`);
509
+ drives.push({
510
+ path: `/dev/${name}`,
511
+ displayName: `/dev/${name} (${displayDetails.join(' • ')})`,
512
+ vendor,
513
+ model,
514
+ size
515
+ });
516
+ });
517
+ return drives;
518
+ }
519
+ catch (error) {
520
+ console.error(chalk_1.default.red(`Error listing drives: ${error.message}`));
521
+ return [];
522
+ }
523
+ }
524
+ else {
525
+ console.log(chalk_1.default.red(`Unsupported platform: ${platform}`));
526
+ return [];
527
+ }
528
+ }
529
+ /**
530
+ * Enhanced checkRequiredTools function that installs missing dependencies
531
+ */
532
+ async function checkAndInstallRequiredTools() {
533
+ const platform = process.platform;
534
+ const inquirer = (await Promise.resolve().then(() => __importStar(require('inquirer')))).default;
535
+ let allToolsAvailable = true;
536
+ // Tool definitions with installation commands per platform
537
+ const requiredTools = [
538
+ {
539
+ name: 'dd',
540
+ description: 'Basic disk writing utility',
541
+ required: true,
542
+ package: {
543
+ darwin: 'coreutils',
544
+ debian: 'coreutils',
545
+ fedora: 'coreutils',
546
+ arch: 'coreutils'
547
+ }
548
+ },
549
+ {
550
+ name: 'gzip',
551
+ aliases: ['gunzip'],
552
+ description: 'Compression utility for .gz files',
553
+ required: true,
554
+ package: {
555
+ darwin: 'gzip',
556
+ debian: 'gzip',
557
+ fedora: 'gzip',
558
+ arch: 'gzip'
559
+ }
560
+ },
561
+ {
562
+ name: 'xz',
563
+ description: 'Compression utility for .xz files',
564
+ required: true,
565
+ package: {
566
+ darwin: 'xz',
567
+ debian: 'xz-utils',
568
+ fedora: 'xz',
569
+ arch: 'xz'
570
+ }
571
+ },
572
+ {
573
+ name: 'pv',
574
+ description: 'Pipe Viewer for progress reporting',
575
+ required: false,
576
+ package: {
577
+ darwin: 'pv',
578
+ debian: 'pv',
579
+ fedora: 'pv',
580
+ arch: 'pv'
581
+ }
582
+ },
583
+ {
584
+ name: 'ddrescue',
585
+ aliases: ['gddrescue'],
586
+ description: 'Enhanced dd with better progress and error handling',
587
+ required: false,
588
+ package: {
589
+ darwin: 'ddrescue',
590
+ debian: 'gddrescue',
591
+ fedora: 'ddrescue',
592
+ arch: 'ddrescue'
593
+ }
594
+ },
595
+ {
596
+ name: 'cmp',
597
+ description: 'Binary comparison utility (for verification)',
598
+ required: false,
599
+ package: {
600
+ darwin: 'diffutils',
601
+ debian: 'diffutils',
602
+ fedora: 'diffutils',
603
+ arch: 'diffutils'
604
+ }
605
+ }
606
+ ];
607
+ // Detect distribution type (for Linux)
608
+ const getDistroType = async () => {
609
+ try {
610
+ if (platform !== 'linux')
611
+ return null;
612
+ if (isCommandAvailable('apt') || isCommandAvailable('apt-get')) {
613
+ return 'debian';
614
+ }
615
+ else if (isCommandAvailable('dnf') || isCommandAvailable('yum')) {
616
+ return 'fedora';
617
+ }
618
+ else if (isCommandAvailable('pacman')) {
619
+ return 'arch';
620
+ }
621
+ return null;
622
+ }
623
+ catch (error) {
624
+ return null;
625
+ }
626
+ };
627
+ // Detect package manager
628
+ const distroType = platform === 'darwin' ? 'darwin' : await getDistroType();
629
+ if (!distroType) {
630
+ console.log(chalk_1.default.yellow('Could not detect your Linux distribution. Automatic installation is not available.'));
631
+ console.log(chalk_1.default.yellow('Please install the required tools manually.'));
632
+ // Still check and report missing tools
633
+ for (const tool of requiredTools) {
634
+ const isAvailable = isCommandAvailable(tool.name) ||
635
+ (tool.aliases && tool.aliases.some(alias => isCommandAvailable(alias)));
636
+ if (!isAvailable) {
637
+ console.log(chalk_1.default.red(`Required tool "${tool.name}" is not available.`));
638
+ if (tool.required)
639
+ allToolsAvailable = false;
640
+ }
641
+ }
642
+ return allToolsAvailable;
643
+ }
644
+ // Determine installation command prefix based on platform and distro
645
+ let installCmdPrefix = '';
646
+ if (platform === 'darwin') {
647
+ if (!isCommandAvailable('brew')) {
648
+ console.log(chalk_1.default.yellow('Homebrew not found. Please install it to automatically install dependencies.'));
649
+ console.log(chalk_1.default.blue('Visit https://brew.sh for installation instructions.'));
650
+ return false;
651
+ }
652
+ installCmdPrefix = 'brew install';
653
+ }
654
+ else if (distroType === 'debian') {
655
+ installCmdPrefix = 'apt-get update && apt-get install -y';
656
+ }
657
+ else if (distroType === 'fedora') {
658
+ installCmdPrefix = 'dnf install -y';
659
+ }
660
+ else if (distroType === 'arch') {
661
+ installCmdPrefix = 'pacman -S --noconfirm';
662
+ }
663
+ // Check each tool and ask to install if missing
664
+ for (const tool of requiredTools) {
665
+ const isAvailable = isCommandAvailable(tool.name) ||
666
+ (tool.aliases && tool.aliases.some(alias => isCommandAvailable(alias)));
667
+ if (!isAvailable) {
668
+ const packageName = tool.package[distroType];
669
+ if (!packageName) {
670
+ console.log(chalk_1.default.yellow(`No package information available for ${tool.name} on your system.`));
671
+ if (tool.required)
672
+ allToolsAvailable = false;
673
+ continue;
674
+ }
675
+ const installCmd = `${installCmdPrefix} ${packageName}`;
676
+ const needsSudo = platform !== 'darwin' && !installCmd.startsWith('sudo');
677
+ const fullInstallCmd = needsSudo ? `sudo ${installCmd}` : installCmd;
678
+ console.log(chalk_1.default.yellow(`${tool.required ? 'Required' : 'Recommended'} tool "${tool.name}" (${tool.description}) is not available.`));
679
+ const { confirmInstall } = await inquirer.prompt([{
680
+ type: 'confirm',
681
+ name: 'confirmInstall',
682
+ message: `Would you like to install ${tool.name} (${packageName}) now?`,
683
+ default: tool.required // Default to yes for required tools
684
+ }]);
685
+ if (confirmInstall) {
686
+ console.log(chalk_1.default.blue(`Installing ${packageName}...`));
687
+ try {
688
+ // Execute the installation command
689
+ await new Promise((resolve, reject) => {
690
+ const childProcess = (0, child_process_1.spawn)(fullInstallCmd, {
691
+ shell: true,
692
+ stdio: 'inherit' // Use inherit for all stdio to properly handle sudo password
693
+ });
694
+ childProcess.on('close', (code) => {
695
+ if (code === 0) {
696
+ resolve();
697
+ }
698
+ else {
699
+ reject(new Error(`Installation failed with code ${code}`));
700
+ }
701
+ });
702
+ childProcess.on('error', (error) => {
703
+ reject(error);
704
+ });
705
+ });
706
+ console.log(chalk_1.default.green(`Successfully installed ${packageName}`));
707
+ // Verify installation
708
+ const isNowAvailable = isCommandAvailable(tool.name) ||
709
+ (tool.aliases && tool.aliases.some(alias => isCommandAvailable(alias)));
710
+ if (!isNowAvailable) {
711
+ console.log(chalk_1.default.red(`Installation completed, but ${tool.name} is still not available in PATH.`));
712
+ console.log(chalk_1.default.yellow('You may need to restart your terminal or update your PATH.'));
713
+ if (tool.required)
714
+ allToolsAvailable = false;
715
+ }
716
+ }
717
+ catch (error) {
718
+ console.log(chalk_1.default.red(`Failed to install ${packageName}: ${error.message}`));
719
+ console.log(chalk_1.default.yellow('Please try to install it manually:'));
720
+ console.log(chalk_1.default.blue(fullInstallCmd));
721
+ if (tool.required)
722
+ allToolsAvailable = false;
723
+ }
724
+ }
725
+ else if (tool.required) {
726
+ console.log(chalk_1.default.yellow(`${tool.name} is required and was not installed. Please install it manually.`));
727
+ allToolsAvailable = false;
728
+ }
729
+ }
730
+ }
731
+ return allToolsAvailable;
732
+ }
733
+ /**
734
+ * Extract a compressed file to a temporary location
735
+ */
736
+ async function extractCompressedFile(sourcePath, isGzipped, isXz, cacheDir) {
737
+ // Generate cache file path with predictable naming
738
+ const sourceFilename = path_1.default.basename(sourcePath);
739
+ const extractedFilename = sourceFilename.replace(/\.(gz|xz)$/, '.extracted');
740
+ const extractedPath = path_1.default.join(cacheDir, extractedFilename);
741
+ // Check if extracted file already exists in the cache
742
+ if (fs_1.default.existsSync(extractedPath)) {
743
+ console.log(chalk_1.default.green(`Found extracted image in cache: ${extractedFilename}`));
744
+ return extractedPath;
745
+ }
746
+ console.log(chalk_1.default.blue(`Extracting compressed file...`));
747
+ // Get source file size for progress estimation
748
+ const sourceSize = fs_1.default.statSync(sourcePath).size;
749
+ const sizeMB = (sourceSize / 1024 / 1024).toFixed(2);
750
+ console.log(chalk_1.default.dim(`Source file size: ${sizeMB} MB`));
751
+ return new Promise((resolve, reject) => {
752
+ let command = '';
753
+ if (isGzipped) {
754
+ // Use pv to show progress if available
755
+ command = isCommandAvailable('pv')
756
+ ? `pv --force --progress --timer --rate --bytes --width 70 "${sourcePath}" | gunzip > "${extractedPath}"`
757
+ : `gunzip -c "${sourcePath}" > "${extractedPath}"`;
758
+ }
759
+ else if (isXz) {
760
+ // Use pv to show progress if available
761
+ command = isCommandAvailable('pv')
762
+ ? `pv --force --progress --timer --rate --bytes --width 70 "${sourcePath}" | xz -dc > "${extractedPath}"`
763
+ : `xz -dc "${sourcePath}" > "${extractedPath}"`;
764
+ }
765
+ else {
766
+ // Not compressed, just return the original
767
+ resolve(sourcePath);
768
+ return;
769
+ }
770
+ // Execute extraction command with proper output handling
771
+ const proc = (0, child_process_1.spawn)('sh', ['-c', command], {
772
+ stdio: ['ignore', 'inherit', 'inherit'] // This will show pv's output directly on the terminal
773
+ });
774
+ // Only show spinner if pv is not available
775
+ let spinnerInterval = null;
776
+ if (!isCommandAvailable('pv')) {
777
+ // Simple progress spinner
778
+ const spinnerChars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
779
+ let spinnerIndex = 0;
780
+ const startTime = Date.now();
781
+ spinnerInterval = setInterval(() => {
782
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
783
+ const minutes = Math.floor(elapsed / 60);
784
+ const seconds = elapsed % 60;
785
+ const timeStr = `${minutes}:${seconds.toString().padStart(2, '0')}`;
786
+ process.stdout.write(`\r${spinnerChars[spinnerIndex]} Extracting compressed file... (${timeStr} elapsed)`);
787
+ spinnerIndex = (spinnerIndex + 1) % spinnerChars.length;
788
+ }, 100);
789
+ proc.on('close', () => {
790
+ if (spinnerInterval) {
791
+ clearInterval(spinnerInterval);
792
+ spinnerInterval = null;
793
+ process.stdout.write('\r✓ Extraction complete! \n');
794
+ }
795
+ });
796
+ }
797
+ proc.on('close', (code) => {
798
+ if (spinnerInterval) {
799
+ clearInterval(spinnerInterval);
800
+ }
801
+ if (code === 0) {
802
+ console.log(chalk_1.default.green(`Extracted file saved to cache for future use: ${extractedFilename}`));
803
+ resolve(extractedPath);
804
+ }
805
+ else {
806
+ // Clean up failed extraction
807
+ try {
808
+ if (fs_1.default.existsSync(extractedPath)) {
809
+ fs_1.default.unlinkSync(extractedPath);
810
+ }
811
+ }
812
+ catch (e) {
813
+ // Ignore cleanup errors
814
+ }
815
+ reject(new Error(`Extraction failed with code ${code}`));
816
+ }
817
+ });
818
+ proc.on('error', (err) => {
819
+ if (spinnerInterval) {
820
+ clearInterval(spinnerInterval);
821
+ }
822
+ console.error(chalk_1.default.red(`\nError during extraction: ${err.message}`));
823
+ reject(err);
824
+ });
825
+ });
826
+ }
827
+ /**
828
+ * Check if a command is available
829
+ */
830
+ function isCommandAvailable(command) {
831
+ try {
832
+ (0, child_process_1.execSync)(`which ${command}`, { stdio: 'ignore' });
833
+ return true;
834
+ }
835
+ catch (error) {
836
+ return false;
837
+ }
838
+ }
839
+ /**
840
+ * Verify the flashed image against the original
841
+ */
842
+ async function verifyFlashedImage(sourcePath, devicePath) {
843
+ console.log(chalk_1.default.blue('\nVerifying flashed image...'));
844
+ return new Promise((resolve, reject) => {
845
+ // Get source file size for verification bounds
846
+ const sourceSize = fs_1.default.statSync(sourcePath).size;
847
+ console.log(chalk_1.default.dim(`Source size: ${(sourceSize / 1024 / 1024).toFixed(2)} MB`));
848
+ let command = '';
849
+ const platform = os_1.default.platform();
850
+ // Use pv if available to show read progress during verification
851
+ if (isCommandAvailable('pv')) {
852
+ if (platform === 'darwin') {
853
+ // macOS: use ddrescue to read from device with pv showing progress
854
+ command = `pv -pterb "${devicePath}" | cmp -n ${sourceSize} "${sourcePath}" -`;
855
+ }
856
+ else {
857
+ // Linux: direct with pv
858
+ command = `pv -pterb < "${devicePath}" | cmp -n ${sourceSize} "${sourcePath}" -`;
859
+ }
860
+ }
861
+ else {
862
+ // Fallback without progress indication
863
+ command = `cmp -n ${sourceSize} "${sourcePath}" "${devicePath}"`;
864
+ }
865
+ console.log(chalk_1.default.dim(`Running: ${command}`));
866
+ const proc = (0, child_process_1.spawn)('sudo', ['sh', '-c', command], {
867
+ stdio: 'inherit' // Use inherit for all stdio to ensure password prompt and progress work correctly
868
+ });
869
+ // Only show spinner if pv is not available
870
+ let spinnerInterval = null;
871
+ if (!isCommandAvailable('pv')) {
872
+ // Simple progress spinner
873
+ const spinnerChars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
874
+ let spinnerIndex = 0;
875
+ const startTime = Date.now();
876
+ spinnerInterval = setInterval(() => {
877
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
878
+ const minutes = Math.floor(elapsed / 60);
879
+ const seconds = elapsed % 60;
880
+ const timeStr = `${minutes}:${seconds.toString().padStart(2, '0')}`;
881
+ process.stdout.write(`\r${spinnerChars[spinnerIndex]} Verifying data... (${timeStr} elapsed)`);
882
+ spinnerIndex = (spinnerIndex + 1) % spinnerChars.length;
883
+ }, 100);
884
+ }
885
+ // If verification takes more than 30 seconds, show a message explaining this can take time
886
+ const timeoutId = setTimeout(() => {
887
+ console.log(chalk_1.default.yellow('\nVerification is still running. This can take several minutes for large images...'));
888
+ }, 30000);
889
+ // When using stdio: 'inherit', we don't need these handlers
890
+ // as stdout and stderr are connected directly to the parent process
891
+ proc.on('close', (code) => {
892
+ if (spinnerInterval) {
893
+ clearInterval(spinnerInterval);
894
+ }
895
+ clearTimeout(timeoutId);
896
+ if (code === 0) {
897
+ if (!isCommandAvailable('pv')) {
898
+ process.stdout.write('\r✓ Verification successful! Data was written correctly. \n');
899
+ }
900
+ else {
901
+ console.log(chalk_1.default.green('Verification successful! Data was written correctly.'));
902
+ }
903
+ resolve();
904
+ }
905
+ else {
906
+ if (!isCommandAvailable('pv')) {
907
+ process.stdout.write('\r✗ Verification failed! Flashed data does not match source.\n');
908
+ }
909
+ else {
910
+ console.log(chalk_1.default.red('Verification failed! Flashed data does not match source.'));
911
+ }
912
+ reject(new Error('Verification failed - flashed data does not match source image'));
913
+ }
914
+ });
915
+ proc.on('error', (err) => {
916
+ if (spinnerInterval) {
917
+ clearInterval(spinnerInterval);
918
+ }
919
+ clearTimeout(timeoutId);
920
+ console.error(chalk_1.default.red(`\nError during verification: ${err.message}`));
921
+ reject(err);
922
+ });
923
+ });
924
+ }
925
+ /**
926
+ * Flash the image to the selected device
927
+ */
928
+ async function flashImageToDevice(imagePath, devicePath, isGzipped, isXz, verify, cacheDir) {
929
+ try {
930
+ const platform = os_1.default.platform();
931
+ const startTime = Date.now();
932
+ // 1. Extract the compressed file if needed
933
+ let extractedPath = imagePath;
934
+ let isExtracted = false;
935
+ if (isGzipped || isXz) {
936
+ extractedPath = await extractCompressedFile(imagePath, isGzipped, isXz, cacheDir);
937
+ isExtracted = extractedPath !== imagePath;
938
+ }
939
+ try {
940
+ // 2. Flash the image
941
+ console.log(chalk_1.default.blue(`\nFlashing image to ${devicePath}...`));
942
+ // Prepare command based on available tools
943
+ let command = '';
944
+ // On macOS, use the raw disk device path (rdiskX instead of diskX)
945
+ let targetDevice = devicePath;
946
+ if (platform === 'darwin' && devicePath.match(/\/dev\/disk\d+$/)) {
947
+ targetDevice = devicePath.replace(/\/dev\/disk(\d+)$/, '/dev/rdisk$1');
948
+ console.log(chalk_1.default.blue(`Using raw device path for macOS: ${targetDevice}`));
949
+ }
950
+ // Determine which tool to use for flashing
951
+ const isOnMacOS = platform === 'darwin';
952
+ const isPvAvailable = isCommandAvailable('pv');
953
+ let ddCommand = '';
954
+ // Always use dd on macOS, regardless of ddrescue availability
955
+ if (isOnMacOS) {
956
+ console.log(chalk_1.default.yellow(`Using dd on macOS for better compatibility...`));
957
+ // On macOS, use the raw disk device (rdiskX instead of diskX) for better performance
958
+ if (isPvAvailable) {
959
+ ddCommand = `pv -pterb "${extractedPath}" | dd of="${targetDevice}" bs=4M`;
960
+ }
961
+ else {
962
+ ddCommand = `dd if="${extractedPath}" of="${targetDevice}" bs=4M`;
963
+ }
964
+ }
965
+ else {
966
+ // On Linux, prefer ddrescue if available
967
+ let ddrescueAvailable = false;
968
+ try {
969
+ ddrescueAvailable = isCommandAvailable('ddrescue') || isCommandAvailable('gddrescue');
970
+ }
971
+ catch (e) {
972
+ ddrescueAvailable = false;
973
+ }
974
+ if (ddrescueAvailable) {
975
+ console.log(chalk_1.default.yellow(`Using ddrescue for enhanced reliability...`));
976
+ ddCommand = await getDdrescueCommand(extractedPath, targetDevice, { isRawDevice: false, useDirectFlag: true });
977
+ }
978
+ else {
979
+ console.log(chalk_1.default.yellow(`ddrescue not available, using dd instead...`));
980
+ if (isPvAvailable) {
981
+ ddCommand = `pv -pterb "${extractedPath}" | dd of="${targetDevice}" bs=4M oflag=direct status=progress`;
982
+ }
983
+ else {
984
+ ddCommand = `dd if="${extractedPath}" of="${targetDevice}" bs=4M oflag=direct status=progress`;
985
+ }
986
+ }
987
+ }
988
+ // Debug: show command
989
+ console.log(chalk_1.default.dim(`Running: ${ddCommand}`));
990
+ // Execute the command with sudo (required for both macOS and Linux)
991
+ try {
992
+ await new Promise((resolve, reject) => {
993
+ // Use inherit for all stdio when using pv to ensure progress display works properly
994
+ const ddProc = (0, child_process_1.spawn)('sudo', ['sh', '-c', ddCommand], {
995
+ stdio: 'inherit' // Use inherit for all stdio to ensure progress display works
996
+ });
997
+ ddProc.on('close', (code) => {
998
+ if (code === 0) {
999
+ console.log(chalk_1.default.green('Flashing completed!'));
1000
+ resolve();
1001
+ }
1002
+ else {
1003
+ reject(new Error(`Flashing failed with code ${code}`));
1004
+ }
1005
+ });
1006
+ ddProc.on('error', (err) => {
1007
+ reject(new Error(`Error executing command: ${err.message}`));
1008
+ });
1009
+ });
1010
+ }
1011
+ catch (error) {
1012
+ // If ddrescue failed with direct disk access error, try with dd instead
1013
+ if (error.message === 'DDRESCUE_DIRECT_ACCESS_ERROR') {
1014
+ console.log(chalk_1.default.yellow('ddrescue could not access disk directly. Falling back to dd...'));
1015
+ // Build a dd command as fallback
1016
+ let fallbackCommand;
1017
+ if (isPvAvailable) {
1018
+ fallbackCommand = `pv -pterb "${extractedPath}" | dd of="${targetDevice}" bs=4M`;
1019
+ }
1020
+ else if (platform === 'darwin') {
1021
+ fallbackCommand = `dd if="${extractedPath}" of="${targetDevice}" bs=4M`;
1022
+ }
1023
+ else {
1024
+ fallbackCommand = `dd if="${extractedPath}" of="${targetDevice}" bs=4M status=progress`;
1025
+ }
1026
+ console.log(chalk_1.default.dim(`Running fallback command: ${fallbackCommand}`));
1027
+ // Execute dd command
1028
+ await new Promise((resolve, reject) => {
1029
+ const ddProc = (0, child_process_1.spawn)('sudo', ['sh', '-c', fallbackCommand], {
1030
+ stdio: 'inherit' // Use inherit for all stdio
1031
+ });
1032
+ ddProc.on('close', (code) => {
1033
+ if (code === 0) {
1034
+ console.log(chalk_1.default.green('Flashing with dd completed!'));
1035
+ resolve();
1036
+ }
1037
+ else {
1038
+ reject(new Error(`Fallback dd command failed with code ${code}`));
1039
+ }
1040
+ });
1041
+ ddProc.on('error', (err) => {
1042
+ reject(new Error(`Error executing dd: ${err.message}`));
1043
+ });
1044
+ });
1045
+ }
1046
+ else {
1047
+ // Re-throw any other errors
1048
+ throw error;
1049
+ }
1050
+ }
1051
+ // Verify the flashed image
1052
+ if (verify) {
1053
+ await verifyFlashedImage(extractedPath, targetDevice);
1054
+ }
1055
+ }
1056
+ catch (error) {
1057
+ console.error(chalk_1.default.red(`\n❗️Error: ${error.message}`));
1058
+ throw error;
1059
+ }
1060
+ }
1061
+ catch (error) {
1062
+ console.error(chalk_1.default.red(`\n❗️Error: ${error.message}`));
1063
+ throw error;
1064
+ }
1065
+ }
1066
+ /**
1067
+ * Get the ddrescue command for enhanced reliability
1068
+ */
1069
+ async function getDdrescueCommand(sourcePath, targetDevice, options) {
1070
+ const platform = os_1.default.platform();
1071
+ const inquirer = (await Promise.resolve().then(() => __importStar(require('inquirer')))).default;
1072
+ // Get source file size for progress estimation
1073
+ const sourceSize = fs_1.default.statSync(sourcePath).size;
1074
+ const sizeMB = (sourceSize / 1024 / 1024).toFixed(2);
1075
+ console.log(chalk_1.default.dim(`Source file size: ${sizeMB} MB`));
1076
+ // Get target device size for verification
1077
+ const targetSize = fs_1.default.statSync(targetDevice).size;
1078
+ const targetSizeMB = (targetSize / 1024 / 1024).toFixed(2);
1079
+ console.log(chalk_1.default.dim(`Target device size: ${targetSizeMB} MB`));
1080
+ // Get user confirmation for flashing
1081
+ const { confirmFlash } = await inquirer.prompt([{
1082
+ type: 'confirm',
1083
+ name: 'confirmFlash',
1084
+ message: `Are you sure you want to flash ${sizeMB} MB of data to ${targetSizeMB} MB of device?`,
1085
+ default: false
1086
+ }]);
1087
+ if (!confirmFlash) {
1088
+ throw new Error('User cancelled flashing');
1089
+ }
1090
+ // Build the ddrescue command
1091
+ let command = '';
1092
+ if (platform === 'darwin') {
1093
+ // macOS: use the raw device path (rdiskX instead of diskX)
1094
+ const rawDevice = targetDevice.replace(/\/dev\/disk(\d+)$/, '/dev/rdisk$1');
1095
+ command = `ddrescue -d -f -b 4M "${sourcePath}" "${rawDevice}" "${targetDevice}"`;
1096
+ }
1097
+ else {
1098
+ // Linux: use the raw device path (rdiskX instead of diskX)
1099
+ const rawDevice = targetDevice.replace(/\/dev\/disk(\d+)$/, '/dev/rdisk$1');
1100
+ command = `ddrescue -d -f -b 4M "${sourcePath}" "${rawDevice}" "${targetDevice}"`;
1101
+ }
1102
+ // Add direct flag if specified
1103
+ if (options.useDirectFlag) {
1104
+ command += ' -D';
1105
+ }
1106
+ // Add raw device flag if specified
1107
+ if (options.isRawDevice) {
1108
+ command += ' -r';
1109
+ }
1110
+ return command;
1111
+ }
1112
+ // Create command
1113
+ const flash = new commander_1.Command('flash')
1114
+ .description('Flash PhyOS image to a removable drive')
1115
+ .arguments('[filename]') // Make argument optional with []
1116
+ .option('--raw', 'Show all disks, including system disks (use with caution)')
1117
+ .option('--no-verify', 'Skip verification after flashing')
1118
+ .action((0, utils_1.handleError)(flashImage));
1119
+ exports.default = flash;
1120
+ //# sourceMappingURL=flash.js.map