@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.
- 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 +322 -0
- package/dist/commands/app/build-container.js.map +1 -0
- package/dist/commands/app/build.js +375 -0
- package/dist/commands/app/build.js.map +1 -0
- package/dist/commands/app/create.js +409 -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 +546 -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 +189 -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,718 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.default = createVm;
|
|
40
|
+
const fs_1 = __importDefault(require("fs"));
|
|
41
|
+
const path_1 = __importDefault(require("path"));
|
|
42
|
+
const child_process_1 = require("child_process");
|
|
43
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
44
|
+
const uuid_1 = require("uuid");
|
|
45
|
+
const rimraf_1 = require("rimraf");
|
|
46
|
+
const vm_1 = require("../../utils/vm");
|
|
47
|
+
/**
|
|
48
|
+
* Determines the architecture to use for the VM
|
|
49
|
+
* Returns the specified architecture if it's valid, or null if no valid arch was provided
|
|
50
|
+
*/
|
|
51
|
+
function validateArch(userArch) {
|
|
52
|
+
if (userArch) {
|
|
53
|
+
if (['amd64', 'arm64'].includes(userArch)) {
|
|
54
|
+
return userArch;
|
|
55
|
+
}
|
|
56
|
+
console.warn(chalk_1.default.yellow(`Unsupported architecture: ${userArch}, please choose a supported option`));
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Detect the host system architecture
|
|
62
|
+
*/
|
|
63
|
+
function getHostArch() {
|
|
64
|
+
const systemArch = process.arch;
|
|
65
|
+
if (systemArch === 'x64')
|
|
66
|
+
return 'amd64';
|
|
67
|
+
if (systemArch === 'arm64')
|
|
68
|
+
return 'arm64';
|
|
69
|
+
console.warn(chalk_1.default.yellow(`Unusual host architecture detected: ${systemArch}`));
|
|
70
|
+
return 'amd64'; // Default to amd64 as fallback
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Gets the download URL and version for PhyOS based on architecture
|
|
74
|
+
*/
|
|
75
|
+
async function getPhyOsDownloadUrl(arch, versionStr) {
|
|
76
|
+
// If a specific version is provided, use it directly (only if it's a valid string)
|
|
77
|
+
if (versionStr && typeof versionStr === 'string' && /^\d+\.\d+\.\d+$/.test(versionStr)) {
|
|
78
|
+
const url = `https://os.phygrid.com/phyos.${versionStr}.qemu${arch}.tar.gz`;
|
|
79
|
+
return { url, version: versionStr };
|
|
80
|
+
}
|
|
81
|
+
// Otherwise, use the short URL and follow redirects to get the latest version
|
|
82
|
+
const shortUrl = `https://qr.run/phyos-qemu${arch}-latest`;
|
|
83
|
+
console.log(chalk_1.default.blue(`Getting latest version URL from ${shortUrl}...`));
|
|
84
|
+
try {
|
|
85
|
+
// Use curl to follow redirect and get the final URL
|
|
86
|
+
const curlOutput = (0, child_process_1.execSync)(`curl -sI "${shortUrl}" | grep -i "^location:" | tail -n1`, { encoding: 'utf8' });
|
|
87
|
+
const redirectUrl = curlOutput.trim().split(/:\s+/)[1];
|
|
88
|
+
if (!redirectUrl) {
|
|
89
|
+
throw new Error('Failed to get redirect URL for PhyOS');
|
|
90
|
+
}
|
|
91
|
+
console.log(chalk_1.default.blue(`Redirect URL: ${redirectUrl}`));
|
|
92
|
+
// Extract version from the URL (format: phyos.1.2.3.qemuXXX.tar.gz)
|
|
93
|
+
const versionMatch = redirectUrl.match(/phyos\.([0-9]+\.[0-9]+\.[0-9]+)\.qemu/i);
|
|
94
|
+
if (!versionMatch || !versionMatch[1]) {
|
|
95
|
+
throw new Error(`Failed to extract version from URL: ${redirectUrl}`);
|
|
96
|
+
}
|
|
97
|
+
const version = versionMatch[1];
|
|
98
|
+
console.log(chalk_1.default.blue(`Detected version: ${version}`));
|
|
99
|
+
return { url: redirectUrl, version };
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
console.error(chalk_1.default.red(`Error following redirect: ${error.message}`));
|
|
103
|
+
throw new Error(`Failed to determine latest PhyOS version: ${error.message}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Downloads a file using curl with progress bar
|
|
108
|
+
*/
|
|
109
|
+
function downloadWithCurl(url, outputPath) {
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
console.log(chalk_1.default.blue(`Downloading from ${url}...`));
|
|
112
|
+
// Create curl process with progress bar
|
|
113
|
+
const curl = (0, child_process_1.spawn)('curl', [
|
|
114
|
+
'--progress-bar', // Show progress bar
|
|
115
|
+
'-L', // Follow redirects
|
|
116
|
+
'-o', outputPath, // Output file
|
|
117
|
+
url // URL to download
|
|
118
|
+
], {
|
|
119
|
+
stdio: ['ignore', 'inherit', 'inherit'] // Show progress in terminal
|
|
120
|
+
});
|
|
121
|
+
curl.on('close', (code) => {
|
|
122
|
+
if (code === 0) {
|
|
123
|
+
resolve();
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
reject(new Error(`curl exited with code ${code}`));
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
curl.on('error', (err) => {
|
|
130
|
+
reject(new Error(`Failed to execute curl: ${err.message}`));
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Copy a file from source to destination
|
|
136
|
+
*/
|
|
137
|
+
function copyFile(sourcePath, destinationPath) {
|
|
138
|
+
fs_1.default.copyFileSync(sourcePath, destinationPath);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Extract tar file using system tar command
|
|
142
|
+
*/
|
|
143
|
+
function extractTarFile(tarFilePath, outputDir) {
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
console.log(chalk_1.default.blue(`Extracting ${path_1.default.basename(tarFilePath)} to ${outputDir}...`));
|
|
146
|
+
// Use system tar command which handles large files better than the Node.js tar library
|
|
147
|
+
const tar = (0, child_process_1.spawn)('tar', [
|
|
148
|
+
'-xzf', // Extract gzipped tar
|
|
149
|
+
tarFilePath, // Input file
|
|
150
|
+
'-C', outputDir // Output directory
|
|
151
|
+
], {
|
|
152
|
+
stdio: ['ignore', 'inherit', 'inherit'] // Show output in terminal
|
|
153
|
+
});
|
|
154
|
+
tar.on('close', (code) => {
|
|
155
|
+
if (code === 0) {
|
|
156
|
+
resolve();
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
reject(new Error(`tar extraction failed with code ${code}`));
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
tar.on('error', (err) => {
|
|
163
|
+
reject(new Error(`Failed to execute tar: ${err.message}`));
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Gets the current disk size in GB
|
|
169
|
+
*/
|
|
170
|
+
function getCurrentDiskSizeGB(diskFilePath) {
|
|
171
|
+
if (!fs_1.default.existsSync(diskFilePath)) {
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
const stats = fs_1.default.statSync(diskFilePath);
|
|
175
|
+
const sizeInBytes = stats.size;
|
|
176
|
+
const sizeInGB = sizeInBytes / (1024 * 1024 * 1024);
|
|
177
|
+
return Math.round(sizeInGB * 100) / 100; // Round to 2 decimal places
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Expands the disk size using dd command
|
|
181
|
+
*/
|
|
182
|
+
function expandDiskSize(diskFilePath, targetSizeGB) {
|
|
183
|
+
return new Promise((resolve, reject) => {
|
|
184
|
+
console.log(chalk_1.default.blue(`Expanding disk to ${targetSizeGB}GB...`));
|
|
185
|
+
// Calculate the additional size needed in bytes
|
|
186
|
+
const currentSizeGB = getCurrentDiskSizeGB(diskFilePath);
|
|
187
|
+
const additionalSizeBytes = Math.ceil((targetSizeGB - currentSizeGB) * 1024 * 1024 * 1024);
|
|
188
|
+
if (additionalSizeBytes <= 0) {
|
|
189
|
+
resolve(); // No expansion needed
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// Use a more compatible dd command approach
|
|
193
|
+
// Calculate block size and count to avoid the '1m' format issue
|
|
194
|
+
const blockSize = 1024 * 1024; // 1MB in bytes
|
|
195
|
+
const count = Math.ceil(additionalSizeBytes / blockSize);
|
|
196
|
+
const dd = (0, child_process_1.spawn)('dd', [
|
|
197
|
+
'if=/dev/zero',
|
|
198
|
+
`bs=${blockSize}`,
|
|
199
|
+
`count=${count}`,
|
|
200
|
+
'>>',
|
|
201
|
+
diskFilePath
|
|
202
|
+
], {
|
|
203
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
204
|
+
shell: true
|
|
205
|
+
});
|
|
206
|
+
dd.on('close', (code) => {
|
|
207
|
+
if (code === 0) {
|
|
208
|
+
console.log(chalk_1.default.green(`✅ Disk expanded to ${targetSizeGB}GB`));
|
|
209
|
+
resolve();
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
reject(new Error(`dd command failed with code ${code}`));
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
dd.on('error', (err) => {
|
|
216
|
+
reject(new Error(`Failed to execute dd: ${err.message}`));
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Downloads and extracts a PhyOS image
|
|
222
|
+
*/
|
|
223
|
+
async function downloadPhyOS(name, arch, versionStr) {
|
|
224
|
+
console.log(chalk_1.default.blue('Determining PhyOS version and download URL...'));
|
|
225
|
+
// Get the download URL and version
|
|
226
|
+
const { url, version: detectedVersion } = await getPhyOsDownloadUrl(arch, versionStr);
|
|
227
|
+
// Get paths for cache and target VM
|
|
228
|
+
const vmDir = (0, vm_1.getVmDir)(name);
|
|
229
|
+
const cacheTarPath = (0, vm_1.getPhyOsCachePath)(detectedVersion, arch, 'tar');
|
|
230
|
+
const cacheImagePath = (0, vm_1.getPhyOsCachePath)(detectedVersion, arch, 'image');
|
|
231
|
+
const cacheDiskPath = (0, vm_1.getPhyOsCachePath)(detectedVersion, arch, 'disk', 'zfs');
|
|
232
|
+
const cacheInitramfsPath = (0, vm_1.getPhyOsCachePath)(detectedVersion, arch, 'initramfs');
|
|
233
|
+
const tarFile = path_1.default.join(vmDir, `phyos.${detectedVersion}.qemu${arch}.tar.gz`);
|
|
234
|
+
const imageFile = path_1.default.join(vmDir, `phyos.${detectedVersion}.qemu${arch}.Image`);
|
|
235
|
+
const diskFile = path_1.default.join(vmDir, `phyos.${detectedVersion}.qemu${arch}.zfs`);
|
|
236
|
+
const initramfsFile = path_1.default.join(vmDir, `phyos.${detectedVersion}.qemu${arch}.cpio.gz`);
|
|
237
|
+
// Ensure the VM directory exists
|
|
238
|
+
if (!fs_1.default.existsSync(vmDir)) {
|
|
239
|
+
fs_1.default.mkdirSync(vmDir, { recursive: true });
|
|
240
|
+
}
|
|
241
|
+
// Check if we need to download or can use cache
|
|
242
|
+
let needsDownload = true;
|
|
243
|
+
let needsExtract = true;
|
|
244
|
+
// Check if cache has the files already
|
|
245
|
+
if (fs_1.default.existsSync(cacheImagePath) && fs_1.default.existsSync(cacheDiskPath) && fs_1.default.existsSync(cacheInitramfsPath)) {
|
|
246
|
+
console.log(chalk_1.default.green(`Found cached PhyOS ${detectedVersion} files, using cache...`));
|
|
247
|
+
needsDownload = false;
|
|
248
|
+
needsExtract = false;
|
|
249
|
+
// Copy files from cache to VM directory
|
|
250
|
+
console.log(chalk_1.default.blue('Copying files from cache to VM directory...'));
|
|
251
|
+
copyFile(cacheImagePath, imageFile);
|
|
252
|
+
copyFile(cacheDiskPath, diskFile);
|
|
253
|
+
copyFile(cacheInitramfsPath, initramfsFile);
|
|
254
|
+
}
|
|
255
|
+
// Check if we have the tar file but still need to extract
|
|
256
|
+
else if (fs_1.default.existsSync(cacheTarPath)) {
|
|
257
|
+
console.log(chalk_1.default.green(`Found cached PhyOS ${detectedVersion} tar file, extracting...`));
|
|
258
|
+
needsDownload = false;
|
|
259
|
+
// Copy tar file from cache to VM directory for extraction
|
|
260
|
+
copyFile(cacheTarPath, tarFile);
|
|
261
|
+
}
|
|
262
|
+
// Download if needed
|
|
263
|
+
if (needsDownload) {
|
|
264
|
+
// Download the tar.gz file using curl with progress bar
|
|
265
|
+
console.log(chalk_1.default.blue(`Downloading PhyOS ${detectedVersion}...`));
|
|
266
|
+
await downloadWithCurl(url, cacheTarPath);
|
|
267
|
+
// Verify the download was successful
|
|
268
|
+
if (!fs_1.default.existsSync(cacheTarPath) || fs_1.default.statSync(cacheTarPath).size === 0) {
|
|
269
|
+
throw new Error('Download failed or resulted in an empty file');
|
|
270
|
+
}
|
|
271
|
+
// Copy to VM directory for extraction
|
|
272
|
+
copyFile(cacheTarPath, tarFile);
|
|
273
|
+
}
|
|
274
|
+
// Extract if needed
|
|
275
|
+
if (needsExtract) {
|
|
276
|
+
console.log(chalk_1.default.green('Extracting files...'));
|
|
277
|
+
try {
|
|
278
|
+
// Extract the tar.gz file using system tar command
|
|
279
|
+
await extractTarFile(tarFile, vmDir);
|
|
280
|
+
// Check if the expected files exist after extraction
|
|
281
|
+
if (!fs_1.default.existsSync(imageFile) || !fs_1.default.existsSync(diskFile) || !fs_1.default.existsSync(initramfsFile)) {
|
|
282
|
+
console.error(chalk_1.default.red(`❗️Required files (${path_1.default.basename(imageFile)}, ${path_1.default.basename(diskFile)}, ${path_1.default.basename(initramfsFile)}) not found in the extracted archive at ${vmDir}`));
|
|
283
|
+
// Clean up potentially corrupt extraction? Maybe remove extracted files or vmDir?
|
|
284
|
+
try {
|
|
285
|
+
if (fs_1.default.existsSync(imageFile))
|
|
286
|
+
fs_1.default.unlinkSync(imageFile);
|
|
287
|
+
if (fs_1.default.existsSync(diskFile))
|
|
288
|
+
fs_1.default.unlinkSync(diskFile);
|
|
289
|
+
if (fs_1.default.existsSync(initramfsFile))
|
|
290
|
+
fs_1.default.unlinkSync(initramfsFile);
|
|
291
|
+
// Consider removing tarFile as well if it came from cache but extraction failed
|
|
292
|
+
// if (!needsDownload && fs.existsSync(tarFile)) fs.unlinkSync(tarFile);
|
|
293
|
+
}
|
|
294
|
+
catch (cleanupError) {
|
|
295
|
+
console.warn(chalk_1.default.yellow(`Warning: Failed to clean up incomplete files: ${cleanupError.message}`));
|
|
296
|
+
}
|
|
297
|
+
throw new Error('Required files not found after extraction');
|
|
298
|
+
}
|
|
299
|
+
// Cache the extracted files
|
|
300
|
+
console.log(chalk_1.default.green('Caching extracted files for future use...'));
|
|
301
|
+
copyFile(imageFile, cacheImagePath);
|
|
302
|
+
copyFile(diskFile, cacheDiskPath);
|
|
303
|
+
copyFile(initramfsFile, cacheInitramfsPath); // Cache initramfs
|
|
304
|
+
}
|
|
305
|
+
catch (extractError) {
|
|
306
|
+
// Log extraction error and re-throw
|
|
307
|
+
console.error(chalk_1.default.red(`Error during extraction: ${extractError.message}`));
|
|
308
|
+
throw extractError; // Re-throw the error to be caught by the caller
|
|
309
|
+
}
|
|
310
|
+
finally {
|
|
311
|
+
// Clean up the downloaded tar file from the VM directory regardless of extraction success/failure
|
|
312
|
+
// unless it was copied from cache initially
|
|
313
|
+
if (needsDownload && fs_1.default.existsSync(tarFile)) {
|
|
314
|
+
try {
|
|
315
|
+
fs_1.default.unlinkSync(tarFile);
|
|
316
|
+
}
|
|
317
|
+
catch (unlinkError) {
|
|
318
|
+
console.warn(chalk_1.default.yellow(`Warning: Failed to remove temporary tar file ${tarFile}: ${unlinkError.message}`));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
console.log(chalk_1.default.green(`PhyOS ${detectedVersion} ready in ${vmDir}`));
|
|
324
|
+
// Return paths to the main files
|
|
325
|
+
return { version: detectedVersion, imageFile, diskFile, initramfsFile }; // Return initramfs path
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Creates a single VM with the specified parameters
|
|
329
|
+
*/
|
|
330
|
+
async function createSingleVm(vmName, arch, memory, headless, versionStr, serialNumber, storageGB) {
|
|
331
|
+
console.log(chalk_1.default.blue(`Creating VM: ${vmName}...`));
|
|
332
|
+
// Download and extract PhyOS
|
|
333
|
+
const downloadedFiles = await downloadPhyOS(vmName, arch, versionStr);
|
|
334
|
+
// Get the actual disk size for display
|
|
335
|
+
let actualDiskSizeGB = getCurrentDiskSizeGB(downloadedFiles.diskFile);
|
|
336
|
+
// Expand disk size if specified
|
|
337
|
+
if (storageGB !== undefined) {
|
|
338
|
+
console.log(chalk_1.default.blue(`Current disk size: ${actualDiskSizeGB}GB`));
|
|
339
|
+
if (storageGB < actualDiskSizeGB) {
|
|
340
|
+
throw new Error(`Cannot shrink disk size. Current size: ${actualDiskSizeGB}GB, requested: ${storageGB}GB. Please specify a size larger than or equal to ${actualDiskSizeGB}GB.`);
|
|
341
|
+
}
|
|
342
|
+
if (storageGB > actualDiskSizeGB) {
|
|
343
|
+
await expandDiskSize(downloadedFiles.diskFile, storageGB);
|
|
344
|
+
// Update the actual disk size for display
|
|
345
|
+
const finalDiskSizeGB = getCurrentDiskSizeGB(downloadedFiles.diskFile);
|
|
346
|
+
actualDiskSizeGB = finalDiskSizeGB;
|
|
347
|
+
}
|
|
348
|
+
// If storageGB === actualDiskSizeGB, no expansion needed
|
|
349
|
+
}
|
|
350
|
+
// Get timestamp for creation
|
|
351
|
+
const createdTime = new Date().toISOString();
|
|
352
|
+
// Create VM configuration (legacy, to maintain backward compatibility)
|
|
353
|
+
const vmConfig = {
|
|
354
|
+
name: vmName,
|
|
355
|
+
version: downloadedFiles.version,
|
|
356
|
+
arch,
|
|
357
|
+
memory,
|
|
358
|
+
image: path_1.default.basename(downloadedFiles.imageFile),
|
|
359
|
+
disk: path_1.default.basename(downloadedFiles.diskFile),
|
|
360
|
+
initramfs: path_1.default.basename(downloadedFiles.initramfsFile),
|
|
361
|
+
created: createdTime
|
|
362
|
+
};
|
|
363
|
+
// Save VM configuration (legacy format)
|
|
364
|
+
const configPath = (0, vm_1.getVmConfigPath)(vmName);
|
|
365
|
+
fs_1.default.writeFileSync(configPath, JSON.stringify(vmConfig, null, 2));
|
|
366
|
+
// Create and save VM settings with memory and serial in the new format
|
|
367
|
+
const vmSettings = {
|
|
368
|
+
memory,
|
|
369
|
+
serial: serialNumber,
|
|
370
|
+
arch,
|
|
371
|
+
version: downloadedFiles.version,
|
|
372
|
+
created: createdTime,
|
|
373
|
+
headless,
|
|
374
|
+
storage: storageGB
|
|
375
|
+
};
|
|
376
|
+
// Save settings to settings.json
|
|
377
|
+
const settingsPath = (0, vm_1.getVmSettingsPath)(vmName);
|
|
378
|
+
fs_1.default.writeFileSync(settingsPath, JSON.stringify(vmSettings, null, 2));
|
|
379
|
+
// For backward compatibility, also save serial to serial.txt
|
|
380
|
+
const serialPath = (0, vm_1.getVmSerialPath)(vmName);
|
|
381
|
+
fs_1.default.writeFileSync(serialPath, serialNumber);
|
|
382
|
+
return { version: downloadedFiles.version, actualDiskSizeGB };
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Command to create one or more PhyOS VMs with interactive prompts for missing options
|
|
386
|
+
*/
|
|
387
|
+
async function createVm(name, options) {
|
|
388
|
+
try {
|
|
389
|
+
// Import inquirer dynamically since it's a heavy dependency
|
|
390
|
+
const inquirer = (await Promise.resolve().then(() => __importStar(require('inquirer')))).default;
|
|
391
|
+
// Ensure VM directory exists
|
|
392
|
+
(0, vm_1.ensureVmDir)();
|
|
393
|
+
// Parse count parameter (default to 1)
|
|
394
|
+
const count = options.count ? parseInt(options.count, 10) : 1;
|
|
395
|
+
if (isNaN(count) || count < 1 || count > 50) {
|
|
396
|
+
throw new Error('Count must be a number between 1 and 50');
|
|
397
|
+
}
|
|
398
|
+
// Get existing VMs for validation and suggestions
|
|
399
|
+
const existingVms = (0, vm_1.listVms)();
|
|
400
|
+
const vmNameValidator = (input) => {
|
|
401
|
+
if (!input || input.trim() === '') {
|
|
402
|
+
return 'VM name is required';
|
|
403
|
+
}
|
|
404
|
+
if (input.includes(' ')) {
|
|
405
|
+
return 'VM name cannot contain spaces';
|
|
406
|
+
}
|
|
407
|
+
if ((0, vm_1.vmExists)(input)) {
|
|
408
|
+
return `VM with name "${input}" already exists`;
|
|
409
|
+
}
|
|
410
|
+
return true;
|
|
411
|
+
};
|
|
412
|
+
// If name is not provided via command line, prompt for it
|
|
413
|
+
if (!name) {
|
|
414
|
+
const answers = await inquirer.prompt([
|
|
415
|
+
{
|
|
416
|
+
type: 'input',
|
|
417
|
+
name: 'name',
|
|
418
|
+
message: count > 1 ? 'Enter a base name for the VMs:' : 'Enter a name for the VM:',
|
|
419
|
+
validate: (input) => {
|
|
420
|
+
if (!input || input.trim() === '') {
|
|
421
|
+
return 'VM name is required';
|
|
422
|
+
}
|
|
423
|
+
if (input.includes(' ')) {
|
|
424
|
+
return 'VM name cannot contain spaces';
|
|
425
|
+
}
|
|
426
|
+
// For multiple VMs, check if any of the suffixed names would conflict
|
|
427
|
+
if (count > 1) {
|
|
428
|
+
for (let i = 1; i <= count; i++) {
|
|
429
|
+
const suffixedName = `${input}-${i}`;
|
|
430
|
+
if ((0, vm_1.vmExists)(suffixedName)) {
|
|
431
|
+
return `VM with name "${suffixedName}" already exists`;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
if ((0, vm_1.vmExists)(input)) {
|
|
437
|
+
return `VM with name "${input}" already exists`;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
]);
|
|
444
|
+
name = answers.name;
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
// Validate the base name and check for conflicts with suffixed names
|
|
448
|
+
if (count > 1) {
|
|
449
|
+
for (let i = 1; i <= count; i++) {
|
|
450
|
+
const suffixedName = `${name}-${i}`;
|
|
451
|
+
if ((0, vm_1.vmExists)(suffixedName)) {
|
|
452
|
+
throw new Error(`VM with name "${suffixedName}" already exists`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
if (vmNameValidator(name) !== true) {
|
|
458
|
+
throw new Error(typeof vmNameValidator(name) === 'string' ? vmNameValidator(name) : 'Invalid VM name');
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
console.log(chalk_1.default.blue(`Creating ${count} PhyOS VM${count > 1 ? 's' : ''} with base name: ${name}`));
|
|
463
|
+
// Prompt for architecture if not provided
|
|
464
|
+
let arch;
|
|
465
|
+
if (!options.arch) {
|
|
466
|
+
const hostArch = getHostArch();
|
|
467
|
+
const answers = await inquirer.prompt([
|
|
468
|
+
{
|
|
469
|
+
type: 'list',
|
|
470
|
+
name: 'arch',
|
|
471
|
+
message: 'Select VM architecture (can be different from host):',
|
|
472
|
+
default: hostArch,
|
|
473
|
+
choices: [
|
|
474
|
+
{
|
|
475
|
+
name: `amd64 (x86_64) ${hostArch === 'amd64' ? '[Host Architecture]' : '[Emulated]'}`,
|
|
476
|
+
value: 'amd64'
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
name: `arm64 (AArch64) ${hostArch === 'arm64' ? '[Host Architecture]' : '[Emulated]'}`,
|
|
480
|
+
value: 'arm64'
|
|
481
|
+
}
|
|
482
|
+
]
|
|
483
|
+
}
|
|
484
|
+
]);
|
|
485
|
+
arch = answers.arch;
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
const validArch = validateArch(options.arch);
|
|
489
|
+
if (!validArch) {
|
|
490
|
+
throw new Error(`Unsupported architecture: ${options.arch}. Supported values are: amd64, arm64`);
|
|
491
|
+
}
|
|
492
|
+
arch = validArch;
|
|
493
|
+
}
|
|
494
|
+
const hostArch = getHostArch();
|
|
495
|
+
const isEmulated = arch !== hostArch;
|
|
496
|
+
console.log(chalk_1.default.blue(`Using architecture: ${arch}${isEmulated ? ' (emulated)' : ' (native)'}`));
|
|
497
|
+
// Force headless mode for cross-architecture emulation in both directions
|
|
498
|
+
const forceHeadless = isEmulated && ((arch === 'amd64' && hostArch === 'arm64') ||
|
|
499
|
+
(arch === 'arm64' && hostArch === 'amd64'));
|
|
500
|
+
// If we're going to force headless mode, inform the user
|
|
501
|
+
if (forceHeadless) {
|
|
502
|
+
console.log(chalk_1.default.yellow(`⚠️ Note: Cross-architecture emulation (${arch} on ${hostArch}) requires headless mode`));
|
|
503
|
+
console.log(chalk_1.default.yellow(` This VM will be configured as headless automatically`));
|
|
504
|
+
}
|
|
505
|
+
// Prompt for version if not provided
|
|
506
|
+
let versionStr;
|
|
507
|
+
if (!options.phyosVersion) {
|
|
508
|
+
const answers = await inquirer.prompt([
|
|
509
|
+
{
|
|
510
|
+
type: 'confirm',
|
|
511
|
+
name: 'useLatest',
|
|
512
|
+
message: 'Use latest PhyOS version?',
|
|
513
|
+
default: true
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
type: 'input',
|
|
517
|
+
name: 'version',
|
|
518
|
+
message: 'Enter specific PhyOS version (e.g. 5.0.127):',
|
|
519
|
+
when: (answers) => !answers.useLatest,
|
|
520
|
+
validate: (input) => {
|
|
521
|
+
if (!input || !/^\d+\.\d+\.\d+$/.test(input)) {
|
|
522
|
+
return 'Please enter a valid version number (format: X.Y.Z)';
|
|
523
|
+
}
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
]);
|
|
528
|
+
versionStr = answers.useLatest ? undefined : answers.version;
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
versionStr = options.phyosVersion;
|
|
532
|
+
}
|
|
533
|
+
// Prompt for memory if not provided
|
|
534
|
+
let memory;
|
|
535
|
+
if (!options.memory) {
|
|
536
|
+
const answers = await inquirer.prompt([
|
|
537
|
+
{
|
|
538
|
+
type: 'input',
|
|
539
|
+
name: 'memory',
|
|
540
|
+
message: 'Enter memory allocation in MB:',
|
|
541
|
+
default: '2048',
|
|
542
|
+
validate: (input) => {
|
|
543
|
+
const mem = parseInt(input, 10);
|
|
544
|
+
if (isNaN(mem) || mem < 512 || mem > 32768) {
|
|
545
|
+
return 'Please enter a valid memory size between 512MB and 32GB';
|
|
546
|
+
}
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
]);
|
|
551
|
+
memory = parseInt(answers.memory, 10);
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
memory = parseInt(options.memory, 10);
|
|
555
|
+
if (isNaN(memory) || memory < 512 || memory > 32768) {
|
|
556
|
+
throw new Error('Memory must be a number between 512MB and 32GB');
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
// Prompt for storage if not provided
|
|
560
|
+
let storageGB;
|
|
561
|
+
if (!options.storage) {
|
|
562
|
+
// For multiple VMs, don't prompt - use current image size
|
|
563
|
+
if (count > 1) {
|
|
564
|
+
storageGB = undefined; // Use current size, no expansion
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
// Get the actual disk size for the default value
|
|
568
|
+
// We need to download/extract first to know the current size
|
|
569
|
+
const tempDownloadedFiles = await downloadPhyOS('temp', arch, versionStr);
|
|
570
|
+
const currentDiskSizeGB = getCurrentDiskSizeGB(tempDownloadedFiles.diskFile);
|
|
571
|
+
// Clean up temp files
|
|
572
|
+
const tempDir = (0, vm_1.getVmDir)('temp');
|
|
573
|
+
if (fs_1.default.existsSync(tempDir)) {
|
|
574
|
+
(0, rimraf_1.rimrafSync)(tempDir);
|
|
575
|
+
}
|
|
576
|
+
const answers = await inquirer.prompt([
|
|
577
|
+
{
|
|
578
|
+
type: 'input',
|
|
579
|
+
name: 'storage',
|
|
580
|
+
message: 'Enter storage allocation in GB (press Enter to use current size):',
|
|
581
|
+
default: String(currentDiskSizeGB),
|
|
582
|
+
validate: (input) => {
|
|
583
|
+
if (!input || input.trim() === '') {
|
|
584
|
+
return true; // Allow empty input to use default
|
|
585
|
+
}
|
|
586
|
+
const storage = parseFloat(input);
|
|
587
|
+
if (isNaN(storage) || storage < 1 || storage > 100) {
|
|
588
|
+
return 'Please enter a valid storage size between 1GB and 100GB';
|
|
589
|
+
}
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
]);
|
|
594
|
+
// If user left it empty or entered the same as current size, don't expand
|
|
595
|
+
if (!answers.storage || answers.storage.trim() === '' || parseFloat(answers.storage) === currentDiskSizeGB) {
|
|
596
|
+
storageGB = undefined; // Use current size, no expansion
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
storageGB = parseFloat(answers.storage);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
storageGB = parseFloat(options.storage);
|
|
605
|
+
if (isNaN(storageGB) || storageGB < 1 || storageGB > 100) {
|
|
606
|
+
throw new Error('Storage must be a number between 1GB and 100GB');
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
// Determine headless mode setting
|
|
610
|
+
let headless;
|
|
611
|
+
// If x86 on ARM, force headless mode
|
|
612
|
+
if (forceHeadless) {
|
|
613
|
+
headless = true;
|
|
614
|
+
}
|
|
615
|
+
// Otherwise use options or default to non-headless
|
|
616
|
+
else if (options.headless !== undefined) {
|
|
617
|
+
headless = options.headless;
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
headless = false; // Default to non-headless mode
|
|
621
|
+
console.log(chalk_1.default.yellow(`⚠️ VM will run with graphical interface (non-headless mode by default)`));
|
|
622
|
+
console.log(chalk_1.default.yellow(` Use --headless flag to run without display`));
|
|
623
|
+
}
|
|
624
|
+
// Create VMs in a loop
|
|
625
|
+
const createdVms = [];
|
|
626
|
+
for (let i = 1; i <= count; i++) {
|
|
627
|
+
const vmName = count > 1 ? `${name}-${i}` : name;
|
|
628
|
+
const serialNumber = `PHY${(0, uuid_1.v4)().replace(/-/g, '').toUpperCase()}`;
|
|
629
|
+
console.log(chalk_1.default.blue(`\n[${i}/${count}] Generating unique serial number for ${vmName}: ${serialNumber}`));
|
|
630
|
+
try {
|
|
631
|
+
const result = await createSingleVm(vmName, arch, memory, headless, versionStr, serialNumber, storageGB);
|
|
632
|
+
createdVms.push({
|
|
633
|
+
name: vmName,
|
|
634
|
+
serial: serialNumber,
|
|
635
|
+
version: result.version,
|
|
636
|
+
diskSizeGB: result.actualDiskSizeGB
|
|
637
|
+
});
|
|
638
|
+
console.log(chalk_1.default.green(`✅ VM "${vmName}" created successfully!`));
|
|
639
|
+
}
|
|
640
|
+
catch (error) {
|
|
641
|
+
console.error(chalk_1.default.red(`❗️Failed to create VM "${vmName}": ${error.message}`));
|
|
642
|
+
// Attempt to clean up VM directory if creation failed
|
|
643
|
+
const vmDir = (0, vm_1.getVmDir)(vmName);
|
|
644
|
+
if (fs_1.default.existsSync(vmDir)) {
|
|
645
|
+
try {
|
|
646
|
+
(0, rimraf_1.rimrafSync)(vmDir);
|
|
647
|
+
console.log(chalk_1.default.yellow(`Cleaned up incomplete VM directory: ${vmDir}`));
|
|
648
|
+
}
|
|
649
|
+
catch (cleanupError) {
|
|
650
|
+
console.warn(chalk_1.default.yellow(`Warning: Failed to clean up VM directory ${vmDir}: ${cleanupError.message}`));
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
// Continue with other VMs instead of exiting
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
if (createdVms.length === 0) {
|
|
658
|
+
throw new Error('Failed to create any VMs');
|
|
659
|
+
}
|
|
660
|
+
// Show success message(s)
|
|
661
|
+
if (count === 1) {
|
|
662
|
+
const vm = createdVms[0];
|
|
663
|
+
const startupInfo = headless
|
|
664
|
+
? `\nSince this VM is in headless mode:
|
|
665
|
+
- Use 'phy vm shell ${vm.name}' to connect to the console
|
|
666
|
+
- Use 'phy vm logs ${vm.name}' to view logs`
|
|
667
|
+
: `\nTo view logs: phy vm logs ${vm.name}`;
|
|
668
|
+
console.log(chalk_1.default.green(`
|
|
669
|
+
✅ PhyOS VM "${vm.name}" (v${vm.version}) has been successfully created!
|
|
670
|
+
|
|
671
|
+
📋 VM Details:
|
|
672
|
+
Memory allocated: ${memory}MB
|
|
673
|
+
Storage allocated: ${vm.diskSizeGB}GB
|
|
674
|
+
Architecture: ${arch}
|
|
675
|
+
Headless mode: ${headless ? 'Yes' : 'No'}
|
|
676
|
+
|
|
677
|
+
🚀 Next steps:
|
|
678
|
+
1. Start the VM: ${chalk_1.default.cyan(`phy vm start ${vm.name}`)}
|
|
679
|
+
2. Add the VM to Phygrid console using this serial number:
|
|
680
|
+
|
|
681
|
+
🆔 Serial Number: ${chalk_1.default.bold(vm.serial)}
|
|
682
|
+
|
|
683
|
+
💡 Use this serial number or the 6 letter provisioning code to connect the VM in the Phygrid console.${startupInfo}
|
|
684
|
+
`));
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
// Multiple VMs success message
|
|
688
|
+
const startupInfo = headless
|
|
689
|
+
? `\nSince these VMs are in headless mode:
|
|
690
|
+
- Use 'phy vm shell <vm-name>' to connect to each console
|
|
691
|
+
- Use 'phy vm logs <vm-name>' to view logs`
|
|
692
|
+
: `\nTo view logs: phy vm logs <vm-name>`;
|
|
693
|
+
console.log(chalk_1.default.green(`
|
|
694
|
+
✅ Successfully created ${createdVms.length} PhyOS VM${createdVms.length > 1 ? 's' : ''}!
|
|
695
|
+
|
|
696
|
+
📋 VM Details:
|
|
697
|
+
Memory allocated: ${memory}MB (each)
|
|
698
|
+
Storage allocated: ${createdVms[0].diskSizeGB}GB (each)
|
|
699
|
+
Architecture: ${arch}
|
|
700
|
+
Headless mode: ${headless ? 'Yes' : 'No'}
|
|
701
|
+
|
|
702
|
+
🚀 Next steps:
|
|
703
|
+
1. Start VMs using: ${chalk_1.default.cyan('phy vm start <vm-name>')}
|
|
704
|
+
2. Add VMs to Phygrid console using their respective serial numbers:
|
|
705
|
+
|
|
706
|
+
🆔 Serial Numbers:
|
|
707
|
+
${createdVms.map(vm => ` ${chalk_1.default.bold(vm.name)}: ${chalk_1.default.bold(vm.serial)}`).join('\n')}
|
|
708
|
+
|
|
709
|
+
💡 Use these serial numbers or the 6 letter provisioning codes to connect the VMs in the Phygrid console.${startupInfo}
|
|
710
|
+
`));
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
catch (error) {
|
|
714
|
+
console.error(chalk_1.default.red(`❗️There was an error: ${error.message}`));
|
|
715
|
+
throw error;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
//# sourceMappingURL=create.js.map
|