@microtronics/studio-cli 0.41.0 → 0.43.0
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/CHANGELOG.md +27 -3
- package/dist/studio-cli.d.ts +5 -0
- package/out/dde/fileGenerators/api.js +43 -4
- package/out/dde/fileGenerators/xml.js +2 -1
- package/out/main.js +28 -0
- package/out/manifest.js +11 -0
- package/out/usb.js +189 -0
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
3
|
+
## 0.43.0 (2026-01-19)
|
|
4
4
|
|
|
5
5
|
### Features
|
|
6
6
|
|
|
7
|
-
* add
|
|
8
|
-
*
|
|
7
|
+
* add debug option to enable mdnCom logging ([34dfd29](https://bitbucket.org/microtronics-core/vscode-studio/commits/34dfd29cf2f5e6776e31463a85f0484a6907b13b))
|
|
8
|
+
* add support for uploading any amx file to an usb device ([e89f108](https://bitbucket.org/microtronics-core/vscode-studio/commits/e89f1087e22e6c0cc88c8b68bbbcc4f1d813dc4f))
|
|
9
|
+
* add USB device detection and listing functionality ([906e3e1](https://bitbucket.org/microtronics-core/vscode-studio/commits/906e3e12bca647f5ceba8b4196f00d507be163d9))
|
|
10
|
+
* add USB device listing command ([fb16e1a](https://bitbucket.org/microtronics-core/vscode-studio/commits/fb16e1ab0c9a52e38467b664e4b2b6adb135a249)), closes [#close](https://bitbucket.org/microtronics-core/vscode-studio/issues/close)
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* ensure proper exit code on successful USB upload ([cc1ae63](https://bitbucket.org/microtronics-core/vscode-studio/commits/cc1ae632669e785017c2afaa689c3750ba22d7fb))
|
|
15
|
+
|
|
16
|
+
## 0.42.0 (2026-01-13)
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* add isIotApp helper function to check IoT app project type ([98ce8a1](https://bitbucket.org/microtronics-core/vscode-studio/commits/98ce8a1358bdd4d2ba1635cd4ef1a3f6e8ec0004))
|
|
21
|
+
* add POST endpoint for addon timeseries data insertion ([ace6880](https://bitbucket.org/microtronics-core/vscode-studio/commits/ace6880f801faefc303a8020c7acf87b06db5709))
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* restrict write access rejection to IoT apps only ([13aa98f](https://bitbucket.org/microtronics-core/vscode-studio/commits/13aa98f261632b576681a894ecb375b7ab5a4b99))
|
|
26
|
+
|
|
27
|
+
## 0.41.0 (2026-01-12)
|
|
28
|
+
|
|
29
|
+
### Features
|
|
30
|
+
|
|
31
|
+
* add validation for maxInstancesPerSite manifest property ([065dc98](https://bitbucket.org/microtronics-core/vscode-studio/commits/065dc9809e8dccb75b74c5b52a58ce4ec15b5860))
|
|
32
|
+
* use manifest maxInstancesPerSite for addon package header ([0350d2a](https://bitbucket.org/microtronics-core/vscode-studio/commits/0350d2af8ee3aae0ddbb00d7af5cd0b1c2c4cdd5))
|
|
9
33
|
* validate manifest before building package ([e70fc0e](https://bitbucket.org/microtronics-core/vscode-studio/commits/e70fc0ef9c89849ce2c19ec0a9bb575a764a3b07))
|
|
10
34
|
|
|
11
35
|
## 0.40.0 (2025-11-11)
|
package/dist/studio-cli.d.ts
CHANGED
|
@@ -2274,6 +2274,11 @@ export declare namespace Manifest {
|
|
|
2274
2274
|
* @param manifest
|
|
2275
2275
|
*/
|
|
2276
2276
|
export function isApp(manifest: Manifest.Manifest): boolean;
|
|
2277
|
+
/**
|
|
2278
|
+
* Check if the current project is an iot app project
|
|
2279
|
+
* @param manifest
|
|
2280
|
+
*/
|
|
2281
|
+
export function isIotApp(manifest: Manifest.Manifest): boolean;
|
|
2277
2282
|
/**
|
|
2278
2283
|
* Check if the current project is an analytics project
|
|
2279
2284
|
* @param manifest
|
|
@@ -495,7 +495,7 @@ function generatePathInformationFromContainer(apiDescription, containerDefinitio
|
|
|
495
495
|
apiPathOptions = [
|
|
496
496
|
{
|
|
497
497
|
path: `/api/1/sites/{siteUid}/addons/{addonUid}/${containerType}/${containerDefinition.title}`,
|
|
498
|
-
params: [addonIdRef],
|
|
498
|
+
params: [contextIdRef, addonIdRef],
|
|
499
499
|
tags: [containerRouteTagName]
|
|
500
500
|
}
|
|
501
501
|
];
|
|
@@ -511,7 +511,8 @@ function generatePathInformationFromContainer(apiDescription, containerDefinitio
|
|
|
511
511
|
apiPathOptions.push(appCenterPathOptions);
|
|
512
512
|
const apiPathMethods = {
|
|
513
513
|
get: undefined,
|
|
514
|
-
put: undefined
|
|
514
|
+
put: undefined,
|
|
515
|
+
post: undefined
|
|
515
516
|
};
|
|
516
517
|
const getMethod = {
|
|
517
518
|
tags: [],
|
|
@@ -610,6 +611,41 @@ function generatePathInformationFromContainer(apiDescription, containerDefinitio
|
|
|
610
611
|
...defaultResponseCodes
|
|
611
612
|
}
|
|
612
613
|
};
|
|
614
|
+
if (isAddonProject) {
|
|
615
|
+
apiPathMethods.post = {
|
|
616
|
+
tags: [],
|
|
617
|
+
summary: `Insert data into the "${tagNameFromContainerTitle(containerDefinition.title)}" container`,
|
|
618
|
+
parameters: [
|
|
619
|
+
{
|
|
620
|
+
name: 'allow_future_date',
|
|
621
|
+
in: 'query',
|
|
622
|
+
description: 'Allow inserting records with a stamp value in the future',
|
|
623
|
+
schema: {
|
|
624
|
+
type: 'boolean'
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
],
|
|
628
|
+
requestBody: {
|
|
629
|
+
content: {
|
|
630
|
+
'application/json': {
|
|
631
|
+
schema: {
|
|
632
|
+
$ref: `#/components/schemas/${containerDefinition.title}`
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
responses: {
|
|
638
|
+
201: {
|
|
639
|
+
...successfulOperationResponse
|
|
640
|
+
},
|
|
641
|
+
400: {
|
|
642
|
+
description: 'Bad request',
|
|
643
|
+
...defaultErrorResponse
|
|
644
|
+
},
|
|
645
|
+
...defaultResponseCodes
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
}
|
|
613
649
|
// only site and addons support timeseries
|
|
614
650
|
const pathOptions = apiPathOptions[0];
|
|
615
651
|
const methods = structuredClone(apiPathMethods);
|
|
@@ -634,7 +670,9 @@ function generatePathInformationFromContainer(apiDescription, containerDefinitio
|
|
|
634
670
|
};
|
|
635
671
|
youngestMethod.get.summary = `Get the youngest record of "${containerDefinition.title}" container`;
|
|
636
672
|
youngestMethod.get.responses['200'] = histdataSingleResponse;
|
|
637
|
-
|
|
673
|
+
const parameterIdx = isAddonProject ? 2 : 1;
|
|
674
|
+
youngestMethod.get.parameters[parameterIdx].schema.$ref =
|
|
675
|
+
`#/components/schemas/${containerDefinition.title}RequestSingle`;
|
|
638
676
|
apiDescription.paths[youngestPath] = youngestMethod;
|
|
639
677
|
const oldestPath = `${pathOptions.path}/oldest`;
|
|
640
678
|
const oldestMethod = {
|
|
@@ -642,7 +680,8 @@ function generatePathInformationFromContainer(apiDescription, containerDefinitio
|
|
|
642
680
|
};
|
|
643
681
|
oldestMethod.get.summary = `Get the oldest record of "${containerDefinition.title}" container`;
|
|
644
682
|
oldestMethod.get.responses['200'] = histdataSingleResponse;
|
|
645
|
-
oldestMethod.get.parameters[
|
|
683
|
+
oldestMethod.get.parameters[parameterIdx].schema.$ref =
|
|
684
|
+
`#/components/schemas/${containerDefinition.title}RequestSingle`;
|
|
646
685
|
apiDescription.paths[oldestPath] = oldestMethod;
|
|
647
686
|
}
|
|
648
687
|
}
|
|
@@ -68,7 +68,8 @@ function generateXMLFromMap(manifest, ddeMap) {
|
|
|
68
68
|
xmlAttr('download', '0');
|
|
69
69
|
}
|
|
70
70
|
// set edit=99 on upward containers to reject write access via API
|
|
71
|
-
|
|
71
|
+
// only for IoT apps and libraries. For addons or analytics this is not needed.
|
|
72
|
+
if ((manifest_1.Manifest.isIotApp(manifest) || manifest_1.Manifest.isLibrary(manifest)) &&
|
|
72
73
|
[TransferDirection.up, TransferDirection.upPlus].includes(containerDefinition.transferDirection)) {
|
|
73
74
|
xmlAttr('edit', '99');
|
|
74
75
|
}
|
package/out/main.js
CHANGED
|
@@ -13,6 +13,7 @@ const update_notifier_1 = __importDefault(require("update-notifier"));
|
|
|
13
13
|
const api_1 = require("./api");
|
|
14
14
|
const log_1 = require("./log");
|
|
15
15
|
const argumentParser_1 = require("./argumentParser");
|
|
16
|
+
const usb_1 = require("./usb");
|
|
16
17
|
const program = new commander_1.Command();
|
|
17
18
|
const notifier = (0, update_notifier_1.default)({ pkg: package_json_1.default });
|
|
18
19
|
const logger = new log_1.Log.Logger();
|
|
@@ -145,6 +146,33 @@ program
|
|
|
145
146
|
const { env, token } = getDefaultOptions(opts);
|
|
146
147
|
main(command, (0, api_1.pack)(cwd, cliFS_1.cliFS, logger, env, token, opts.phase));
|
|
147
148
|
});
|
|
149
|
+
// USB commands
|
|
150
|
+
const usbCommand = program.command('usb').description('USB device management commands');
|
|
151
|
+
usbCommand
|
|
152
|
+
.command('list')
|
|
153
|
+
.description('List available USB devices')
|
|
154
|
+
.action(async () => {
|
|
155
|
+
await (0, usb_1.printUsbDevices)(logger);
|
|
156
|
+
notifier.notify();
|
|
157
|
+
});
|
|
158
|
+
usbCommand
|
|
159
|
+
.command('upload')
|
|
160
|
+
.description('Upload an AMX binary to a USB device')
|
|
161
|
+
.requiredOption('-s, --serial <serial>', 'Device serial number (16 characters hex)')
|
|
162
|
+
.requiredOption('-f, --file <path>', 'Path to the AMX file (relative to current directory)')
|
|
163
|
+
.requiredOption('--debug', 'Enable debug logging', false)
|
|
164
|
+
.action(async (opts) => {
|
|
165
|
+
const fileUri = vscode_uri_1.Utils.joinPath(cwd, opts.file);
|
|
166
|
+
const result = await (0, usb_1.uploadToDevice)(opts.serial, fileUri, cliFS_1.cliFS, logger, opts.debug);
|
|
167
|
+
notifier.notify();
|
|
168
|
+
if (!result.success) {
|
|
169
|
+
logger.error(result.message);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
process.exit(0);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
148
176
|
module.exports = function (argv) {
|
|
149
177
|
program.parse(argv);
|
|
150
178
|
};
|
package/out/manifest.js
CHANGED
|
@@ -161,6 +161,17 @@ var Manifest;
|
|
|
161
161
|
return manifest.type === ProjectTypes.app;
|
|
162
162
|
}
|
|
163
163
|
Manifest.isApp = isApp;
|
|
164
|
+
/**
|
|
165
|
+
* Check if the current project is an iot app project
|
|
166
|
+
* @param manifest
|
|
167
|
+
*/
|
|
168
|
+
function isIotApp(manifest) {
|
|
169
|
+
if (manifest.type === undefined) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return manifest.type === ProjectTypes.app;
|
|
173
|
+
}
|
|
174
|
+
Manifest.isIotApp = isIotApp;
|
|
164
175
|
/**
|
|
165
176
|
* Check if the current project is an analytics project
|
|
166
177
|
* @param manifest
|
package/out/usb.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.listUsbDevices = listUsbDevices;
|
|
4
|
+
exports.printUsbDevices = printUsbDevices;
|
|
5
|
+
exports.findDeviceBySerialNumber = findDeviceBySerialNumber;
|
|
6
|
+
exports.uploadToDevice = uploadToDevice;
|
|
7
|
+
const usb_1 = require("usb");
|
|
8
|
+
const uto_master_1 = require("uto-master");
|
|
9
|
+
const utoStack_1 = require("uto-master/dist/src/utilities/utoStack");
|
|
10
|
+
// Microtronics vendor ID
|
|
11
|
+
const MICROTRONICS_VENDOR_ID = 0x2544;
|
|
12
|
+
/**
|
|
13
|
+
* Custom WebUSB instance configured to allow Microtronics devices
|
|
14
|
+
*/
|
|
15
|
+
const customWebUSB = new usb_1.WebUSB({
|
|
16
|
+
// Bypass checking for authorized devices
|
|
17
|
+
allowedDevices: [
|
|
18
|
+
{
|
|
19
|
+
vendorId: MICROTRONICS_VENDOR_ID
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* Lists all available USB devices filtered by Microtronics vendor ID
|
|
25
|
+
* @returns Promise resolving to an array of USB device information
|
|
26
|
+
*/
|
|
27
|
+
async function listUsbDevices() {
|
|
28
|
+
const devices = await customWebUSB.getDevices();
|
|
29
|
+
return devices.map((device) => ({
|
|
30
|
+
productName: device.productName,
|
|
31
|
+
serialNumber: device.serialNumber || device.productName?.split(' ').pop(),
|
|
32
|
+
vendorId: device.vendorId,
|
|
33
|
+
productId: device.productId
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Prints USB device product names using the provided logger
|
|
38
|
+
* @param logger - The logger instance to use for output
|
|
39
|
+
*/
|
|
40
|
+
async function printUsbDevices(logger) {
|
|
41
|
+
const devices = await listUsbDevices();
|
|
42
|
+
if (devices.length === 0) {
|
|
43
|
+
logger.info('No USB devices found.');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
logger.info('Available USB devices:');
|
|
47
|
+
devices.forEach((device, index) => {
|
|
48
|
+
const name = device.productName ||
|
|
49
|
+
`Unknown Device (VID: 0x${device.vendorId.toString(16)}, PID: 0x${device.productId.toString(16)})`;
|
|
50
|
+
const serial = device.serialNumber ? ` [${device.serialNumber}]` : '';
|
|
51
|
+
logger.log(` ${index + 1}. ${name}${serial}`);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Find a USB device by its serial number
|
|
56
|
+
* @param serialNumber - The 16-character hex serial number of the device
|
|
57
|
+
* @returns The USB device or null if not found
|
|
58
|
+
*/
|
|
59
|
+
async function findDeviceBySerialNumber(serialNumber) {
|
|
60
|
+
const devices = await customWebUSB.getDevices();
|
|
61
|
+
for (const device of devices) {
|
|
62
|
+
// Check direct serial number or serial from product name
|
|
63
|
+
const deviceSerial = device.serialNumber || device.productName?.split(' ').pop();
|
|
64
|
+
if (deviceSerial?.toLowerCase() === serialNumber.toLowerCase()) {
|
|
65
|
+
return device;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Upload an AMX binary file to a USB device
|
|
72
|
+
* @param serialNumber - The serial number of the target device
|
|
73
|
+
* @param fileUri - The URI to the AMX file
|
|
74
|
+
* @param fs - The filesystem interface for file operations
|
|
75
|
+
* @param logger - The logger instance for output
|
|
76
|
+
* @param debugLog - Enable debug logging
|
|
77
|
+
* @returns Promise resolving to an UploadResult
|
|
78
|
+
*/
|
|
79
|
+
async function uploadToDevice(serialNumber, fileUri, fs, logger, debugLog = false) {
|
|
80
|
+
// Check if file exists
|
|
81
|
+
const fileStat = await fs.stat(fileUri);
|
|
82
|
+
if (!fileStat) {
|
|
83
|
+
return {
|
|
84
|
+
success: false,
|
|
85
|
+
message: `File not found: ${fileUri.fsPath}`
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// Find the device by serial number
|
|
89
|
+
logger.info(`Searching for device with serial number: ${serialNumber}`);
|
|
90
|
+
const device = await findDeviceBySerialNumber(serialNumber);
|
|
91
|
+
if (!device) {
|
|
92
|
+
return {
|
|
93
|
+
success: false,
|
|
94
|
+
message: `Device not found with serial number: ${serialNumber}`
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
logger.info(`Device found: ${device.productName}`);
|
|
98
|
+
// Read the file contents
|
|
99
|
+
const fileData = (await fs.readFile(fileUri));
|
|
100
|
+
// Initialize UTO Master
|
|
101
|
+
const utoMaster = new uto_master_1.UtoMaster();
|
|
102
|
+
let deviceConnected = false;
|
|
103
|
+
try {
|
|
104
|
+
await utoMaster.init();
|
|
105
|
+
// Set up UTO transmit callback
|
|
106
|
+
utoMaster.utoTx(async (data) => {
|
|
107
|
+
if (device.opened) {
|
|
108
|
+
await device.transferOut(uto_master_1.USB_CONFIGURATION.utoEndpoint, data);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
if (debugLog) {
|
|
112
|
+
utoMaster.on('mdnCom', (message) => {
|
|
113
|
+
logger.log('mdnCom', message);
|
|
114
|
+
});
|
|
115
|
+
utoMaster.on('mdnComError', (message) => {
|
|
116
|
+
logger.log('mdnComError', message);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
// Set up progress reporting
|
|
120
|
+
let lastProgress = -1;
|
|
121
|
+
utoMaster.on('installing', (data) => {
|
|
122
|
+
if (!data) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// /*const progress = Math.round(data.progress);
|
|
126
|
+
if (data !== lastProgress) {
|
|
127
|
+
lastProgress = data;
|
|
128
|
+
logger.info(`Upload progress: ${data}%`);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
// Open device and claim interfaces
|
|
132
|
+
await device.open();
|
|
133
|
+
await device.selectConfiguration(uto_master_1.USB_CONFIGURATION.configurationNumber);
|
|
134
|
+
await device.claimInterface(uto_master_1.USB_CONFIGURATION.utoInterface);
|
|
135
|
+
deviceConnected = true;
|
|
136
|
+
logger.info('Device connected, starting upload...');
|
|
137
|
+
// Start UTO polling for receiving data
|
|
138
|
+
startUtoPolling(device, utoMaster, () => deviceConnected);
|
|
139
|
+
await utoMaster.requestDeviceInfo();
|
|
140
|
+
// Upload the file
|
|
141
|
+
await utoMaster.sendFile(utoStack_1.UtoFileType.pawn, new Uint8Array(fileData.buffer, fileData.byteOffset, fileData.length));
|
|
142
|
+
logger.done('Upload completed successfully!');
|
|
143
|
+
return {
|
|
144
|
+
success: true,
|
|
145
|
+
message: 'File uploaded successfully'
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
150
|
+
return {
|
|
151
|
+
success: false,
|
|
152
|
+
message: `Upload failed: ${errorMessage}`
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
// Clean up - close device
|
|
157
|
+
deviceConnected = false;
|
|
158
|
+
if (device.opened) {
|
|
159
|
+
try {
|
|
160
|
+
await device.close().catch();
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Ignore close errors
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Start polling for UTO data from the device
|
|
170
|
+
* @param device - The USB device
|
|
171
|
+
* @param utoMaster - The UTO Master instance
|
|
172
|
+
* @param isConnected - Function that returns connection status
|
|
173
|
+
*/
|
|
174
|
+
async function startUtoPolling(device, utoMaster, isConnected) {
|
|
175
|
+
while (isConnected()) {
|
|
176
|
+
try {
|
|
177
|
+
const result = await device.transferIn(uto_master_1.USB_CONFIGURATION.utoEndpoint, uto_master_1.USB_CONFIGURATION.utoSize);
|
|
178
|
+
if (result.status === 'ok' && result.data) {
|
|
179
|
+
const uint8Data = new Uint8Array(result.data.buffer);
|
|
180
|
+
await utoMaster.utoRx(uint8Data);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// Connection lost or polling stopped
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=usb.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@microtronics/studio-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
4
4
|
"description": "Microtronics Studio CLI Tool",
|
|
5
5
|
"main": "./out/api.js",
|
|
6
6
|
"typings": "./dist/studio-cli.d.ts",
|
|
@@ -45,6 +45,8 @@
|
|
|
45
45
|
"semver": "^7.7.2",
|
|
46
46
|
"tinytar-fix": "^0.1.1",
|
|
47
47
|
"update-notifier": "^7.3.1",
|
|
48
|
+
"usb": "^2.14.0",
|
|
49
|
+
"uto-master": "bitbucket:microtronics-core/wasm-uto-master",
|
|
48
50
|
"vscode-uri": "^3.0.8",
|
|
49
51
|
"yaml": "^2.6.1"
|
|
50
52
|
},
|