@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.
- package/LICENSE.md +19 -0
- package/README.md +24 -0
- package/bin/index.js +2 -0
- package/dist/commands/app/build-apps.js +66 -0
- package/dist/commands/app/build-apps.js.map +1 -0
- package/dist/commands/app/build-container-remote.js +171 -0
- package/dist/commands/app/build-container-remote.js.map +1 -0
- package/dist/commands/app/build-container.js +330 -0
- package/dist/commands/app/build-container.js.map +1 -0
- package/dist/commands/app/build.js +393 -0
- package/dist/commands/app/build.js.map +1 -0
- package/dist/commands/app/create.js +451 -0
- package/dist/commands/app/create.js.map +1 -0
- package/dist/commands/app/deploy.js +176 -0
- package/dist/commands/app/deploy.js.map +1 -0
- package/dist/commands/app/device-picker.js +150 -0
- package/dist/commands/app/device-picker.js.map +1 -0
- package/dist/commands/app/import.js +303 -0
- package/dist/commands/app/import.js.map +1 -0
- package/dist/commands/app/list.js +26 -0
- package/dist/commands/app/list.js.map +1 -0
- package/dist/commands/app/publish.js +327 -0
- package/dist/commands/app/publish.js.map +1 -0
- package/dist/commands/app/settings.js +129 -0
- package/dist/commands/app/settings.js.map +1 -0
- package/dist/commands/app/types.js +13 -0
- package/dist/commands/app/types.js.map +1 -0
- package/dist/commands/app/upload-description.js +139 -0
- package/dist/commands/app/upload-description.js.map +1 -0
- package/dist/commands/app/upload-settings.js +29 -0
- package/dist/commands/app/upload-settings.js.map +1 -0
- package/dist/commands/app/utils.js +86 -0
- package/dist/commands/app/utils.js.map +1 -0
- package/dist/commands/auth/login.js +111 -0
- package/dist/commands/auth/login.js.map +1 -0
- package/dist/commands/auth/logout.js +19 -0
- package/dist/commands/auth/logout.js.map +1 -0
- package/dist/commands/descriptor/create.js +143 -0
- package/dist/commands/descriptor/create.js.map +1 -0
- package/dist/commands/descriptor/index.js +36 -0
- package/dist/commands/descriptor/index.js.map +1 -0
- package/dist/commands/descriptor/publish.js +163 -0
- package/dist/commands/descriptor/publish.js.map +1 -0
- package/dist/commands/descriptor/show.js +68 -0
- package/dist/commands/descriptor/show.js.map +1 -0
- package/dist/commands/dev/develop.js +175 -0
- package/dist/commands/dev/develop.js.map +1 -0
- package/dist/commands/dev/forward.js +118 -0
- package/dist/commands/dev/forward.js.map +1 -0
- package/dist/commands/dev/index.js +66 -0
- package/dist/commands/dev/index.js.map +1 -0
- package/dist/commands/dev/list.js +96 -0
- package/dist/commands/dev/list.js.map +1 -0
- package/dist/commands/dev/screen-devtools.js +156 -0
- package/dist/commands/dev/screen-devtools.js.map +1 -0
- package/dist/commands/dev/select.js +118 -0
- package/dist/commands/dev/select.js.map +1 -0
- package/dist/commands/dev/shell.js +171 -0
- package/dist/commands/dev/shell.js.map +1 -0
- package/dist/commands/dev/vnc.js +75 -0
- package/dist/commands/dev/vnc.js.map +1 -0
- package/dist/commands/device/select.js +118 -0
- package/dist/commands/device/select.js.map +1 -0
- package/dist/commands/flash.js +1120 -0
- package/dist/commands/flash.js.map +1 -0
- package/dist/commands/inst/create.js +55 -0
- package/dist/commands/inst/create.js.map +1 -0
- package/dist/commands/inst/index.js +15 -0
- package/dist/commands/inst/index.js.map +1 -0
- package/dist/commands/inst/list.js +26 -0
- package/dist/commands/inst/list.js.map +1 -0
- package/dist/commands/legacydev/debug.js +11 -0
- package/dist/commands/legacydev/debug.js.map +1 -0
- package/dist/commands/legacydev/deploy.js +15 -0
- package/dist/commands/legacydev/deploy.js.map +1 -0
- package/dist/commands/legacydev/dumpTwin.js +27 -0
- package/dist/commands/legacydev/dumpTwin.js.map +1 -0
- package/dist/commands/legacydev/forward.js +104 -0
- package/dist/commands/legacydev/forward.js.map +1 -0
- package/dist/commands/legacydev/index.js +188 -0
- package/dist/commands/legacydev/index.js.map +1 -0
- package/dist/commands/legacydev/invoke.js +29 -0
- package/dist/commands/legacydev/invoke.js.map +1 -0
- package/dist/commands/legacydev/js.js +69 -0
- package/dist/commands/legacydev/js.js.map +1 -0
- package/dist/commands/legacydev/list.js +196 -0
- package/dist/commands/legacydev/list.js.map +1 -0
- package/dist/commands/legacydev/logs.js +60 -0
- package/dist/commands/legacydev/logs.js.map +1 -0
- package/dist/commands/legacydev/modules.js +50 -0
- package/dist/commands/legacydev/modules.js.map +1 -0
- package/dist/commands/legacydev/move.js +23 -0
- package/dist/commands/legacydev/move.js.map +1 -0
- package/dist/commands/legacydev/ota.js +88 -0
- package/dist/commands/legacydev/ota.js.map +1 -0
- package/dist/commands/legacydev/patchTwin.js +21 -0
- package/dist/commands/legacydev/patchTwin.js.map +1 -0
- package/dist/commands/legacydev/pin.js +23 -0
- package/dist/commands/legacydev/pin.js.map +1 -0
- package/dist/commands/legacydev/pub.js +25 -0
- package/dist/commands/legacydev/pub.js.map +1 -0
- package/dist/commands/legacydev/rdp.js +64 -0
- package/dist/commands/legacydev/rdp.js.map +1 -0
- package/dist/commands/legacydev/screen-devtools.js +142 -0
- package/dist/commands/legacydev/screen-devtools.js.map +1 -0
- package/dist/commands/legacydev/settingsShow.js +89 -0
- package/dist/commands/legacydev/settingsShow.js.map +1 -0
- package/dist/commands/legacydev/settingsUpdate.js +114 -0
- package/dist/commands/legacydev/settingsUpdate.js.map +1 -0
- package/dist/commands/legacydev/shell.js +167 -0
- package/dist/commands/legacydev/shell.js.map +1 -0
- package/dist/commands/legacydev/showTwin.js +9 -0
- package/dist/commands/legacydev/showTwin.js.map +1 -0
- package/dist/commands/legacydev/statusLog.js +56 -0
- package/dist/commands/legacydev/statusLog.js.map +1 -0
- package/dist/commands/legacydev/sub.js +39 -0
- package/dist/commands/legacydev/sub.js.map +1 -0
- package/dist/commands/legacydev/vnc.js +61 -0
- package/dist/commands/legacydev/vnc.js.map +1 -0
- package/dist/commands/tenant/index.js +21 -0
- package/dist/commands/tenant/index.js.map +1 -0
- package/dist/commands/tenant/list.js +14 -0
- package/dist/commands/tenant/list.js.map +1 -0
- package/dist/commands/tenant/select.js +87 -0
- package/dist/commands/tenant/select.js.map +1 -0
- package/dist/commands/vm/create.js +718 -0
- package/dist/commands/vm/create.js.map +1 -0
- package/dist/commands/vm/index.js +130 -0
- package/dist/commands/vm/index.js.map +1 -0
- package/dist/commands/vm/list.js +124 -0
- package/dist/commands/vm/list.js.map +1 -0
- package/dist/commands/vm/logs.js +66 -0
- package/dist/commands/vm/logs.js.map +1 -0
- package/dist/commands/vm/remove.js +180 -0
- package/dist/commands/vm/remove.js.map +1 -0
- package/dist/commands/vm/shell.js +400 -0
- package/dist/commands/vm/shell.js.map +1 -0
- package/dist/commands/vm/start.js +861 -0
- package/dist/commands/vm/start.js.map +1 -0
- package/dist/commands/vm/stop.js +232 -0
- package/dist/commands/vm/stop.js.map +1 -0
- package/dist/index.js +158 -0
- package/dist/index.js.map +1 -0
- package/dist/services/admin-api/admin-api.types.js +3 -0
- package/dist/services/admin-api/admin-api.types.js.map +1 -0
- package/dist/services/admin-api/device-modules.admin-api.service.js +58 -0
- package/dist/services/admin-api/device-modules.admin-api.service.js.map +1 -0
- package/dist/services/admin-api/devices-admin-api.service.js +213 -0
- package/dist/services/admin-api/devices-admin-api.service.js.map +1 -0
- package/dist/services/admin-api/gridapps-admin-api.service.js +59 -0
- package/dist/services/admin-api/gridapps-admin-api.service.js.map +1 -0
- package/dist/services/admin-api/index.js +157 -0
- package/dist/services/admin-api/index.js.map +1 -0
- package/dist/services/admin-api/installations-admin-api.service.js +29 -0
- package/dist/services/admin-api/installations-admin-api.service.js.map +1 -0
- package/dist/services/admin-api/organizations-admin-api.service.js +53 -0
- package/dist/services/admin-api/organizations-admin-api.service.js.map +1 -0
- package/dist/services/auth/device-grant-auth.service.js +224 -0
- package/dist/services/auth/device-grant-auth.service.js.map +1 -0
- package/dist/services/phyhub/index.js +200 -0
- package/dist/services/phyhub/index.js.map +1 -0
- package/dist/services/phyhub/phyhub.types.js +3 -0
- package/dist/services/phyhub/phyhub.types.js.map +1 -0
- package/dist/utils/device-fetcher.js +92 -0
- package/dist/utils/device-fetcher.js.map +1 -0
- package/dist/utils/devices.js +41 -0
- package/dist/utils/devices.js.map +1 -0
- package/dist/utils/docker-credentials.js +720 -0
- package/dist/utils/docker-credentials.js.map +1 -0
- package/dist/utils/emulated-device.js +91 -0
- package/dist/utils/emulated-device.js.map +1 -0
- package/dist/utils/index.js +180 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/modules.js +36 -0
- package/dist/utils/modules.js.map +1 -0
- package/dist/utils/org-selector.js +108 -0
- package/dist/utils/org-selector.js.map +1 -0
- package/dist/utils/proxy.js +31 -0
- package/dist/utils/proxy.js.map +1 -0
- package/dist/utils/registry-credentials.js +113 -0
- package/dist/utils/registry-credentials.js.map +1 -0
- package/dist/utils/statuses.js +124 -0
- package/dist/utils/statuses.js.map +1 -0
- package/dist/utils/templates.js +197 -0
- package/dist/utils/templates.js.map +1 -0
- package/dist/utils/tenant-storage.js +88 -0
- package/dist/utils/tenant-storage.js.map +1 -0
- package/dist/utils/vm.js +434 -0
- package/dist/utils/vm.js.map +1 -0
- package/dist/utils/with-spinner.js +20 -0
- package/dist/utils/with-spinner.js.map +1 -0
- 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
|