Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
"particle-api-js": "^11.1.7",
"particle-commands": "^1.0.6",
"particle-library-manager": "^1.0.6",
"particle-usb": "^4.0.1",
"particle-usb": "^4.1.0",
"request": "^2.88.2",
"safe-buffer": "^5.2.0",
"semver": "^7.5.2",
Expand Down
13 changes: 13 additions & 0 deletions src/cli/usb.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,19 @@ module.exports = ({ commandProcessor, root }) => {
}
});

commandProcessor.createCommand(usb, 'env', 'Gets environment variables from a device', {
params: '[devices...]',
options: commonOptions,
examples: {
'$0 $command': 'Gets environment variables from the connected device',
'$0 $command --all': 'Gets environment variables from all devices connected over USB',
'$0 $command my_device': 'Gets environment variables from the device named "my_device"'
},
handler: (args) => {
return usbCommand().getEnv(args);
}
});

return usb;
};

1 change: 1 addition & 0 deletions src/cli/usb.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ describe('USB Command-Line Interface', () => {
' configure Update the system USB configuration',
' cloud-status Check a device\'s cloud connection state',
' network-interfaces Gets the network configuration of the device',
' env Gets environment variables from a device',
''
Comment on lines 42 to 44
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new usb env subcommand is added to the top-level help text, but there are no CLI parsing/help tests specifically for usb env (unlike other subcommands in this file, e.g. usb network-interfaces). Add a describe('Handles usb env Command' ...) block to verify argument parsing (devices..., --all) and --help output/examples, so regressions in the command definition are caught.

Copilot uses AI. Check for mistakes.
].join('\n'));
});
Expand Down
72 changes: 72 additions & 0 deletions src/cmd/usb.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use strict';
const os = require('os');
const { asyncMapSeries, buildDeviceFilter } = require('../lib/utilities');
const { getDevice, formatDeviceInfo } = require('./device-util');
const { getUsbDevices, openUsbDevice, TimeoutError, DeviceProtectionError, forEachUsbDevice, executeWithUsbDevice } = require('./usb-util');
Expand Down Expand Up @@ -118,6 +119,77 @@ module.exports = class UsbCommand extends CLICommandBase {
});
}

getEnv(args) {
args.api = this._api;
args.auth = this._auth;
const output = [];

return forEachUsbDevice(args, async (usbDevice) => {
const result = await usbDevice.getEnv();
const platform = platformForId(usbDevice.platformId);
const formattedOutput = this._formatEnvOutput(result, platform.displayName, usbDevice.id);
output.push(...formattedOutput);
return result;
}).then(() => {
output.forEach(line => console.log(line));
});
}

_formatEnvOutput(result, platformName, deviceId) {
const output = [];
const push = line => output.push(line);

push('');
push(`${chalk.bold('Device:')} ${chalk.cyan(deviceId)} (${chalk.cyan(platformName)})`);

if (result.snapshot) {
push(`${chalk.bold('Snapshot Hash:')} ${chalk.gray(result.snapshot.hash)}`);
}

const envVars = result.env ?? {};
const entries = Object.entries(envVars);

if (entries.length === 0) {
push(chalk.yellow(' No environment variables set'));
push('');
return output;
}

push(chalk.bold(`${os.EOL}Environment Variables:`));

const { appVars, systemVars } = this._groupEnvVars(entries);

this._renderEnvGroup(output, ' Firmware:', appVars, 'green');
appVars.length > 0 && systemVars.length > 0 && push('');
this._renderEnvGroup(output, ' Cloud:', systemVars, 'cyan');
Comment on lines +158 to +164
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_formatEnvOutput() pushes a string that begins with os.EOL ("\nEnvironment Variables:"). Since callers print each element with console.log(line), embedding a newline inside a single “line” will produce an extra blank line and make output harder to reason about (and can behave oddly on Windows with \r\n). Prefer pushing an empty string as its own array element (or reusing the existing push('')) and then pushing 'Environment Variables:' as a standalone line (which would also let you drop the os dependency here).

Copilot uses AI. Check for mistakes.

push('');
return output;
}

_groupEnvVars(entries) {
return entries.reduce(
(acc, [key, { value, isApp }]) => {
const target = isApp ? acc.appVars : acc.systemVars;
target.push({ key, value });
return acc;
},
{ appVars: [], systemVars: [] }
);
}

_renderEnvGroup(output, title, vars, color) {
if (vars.length === 0) {
return;
}
output.push(chalk.dim(title));
vars.sort((a, b) => a.key.localeCompare(b.key))
.forEach(({ key, value }) => {
output.push(` ${chalk[color](key)}=${chalk.white(value)}`);
});

}

stopListening(args) {
args.api = this._api;
args.auth = this._auth;
Expand Down
172 changes: 172 additions & 0 deletions src/cmd/usb.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use strict';
const os = require('os');
const { expect } = require('../../test/setup');
const { default: stripAnsi } = require('strip-ansi');
const UsbCommands = require('./usb');
Expand Down Expand Up @@ -111,4 +112,175 @@ describe('USB Commands', () => {
expect(res.map(stripAnsi)).to.eql(expectedOutput);
});
});

describe('_formatEnvOutput', () => {
let usbCommands;

beforeEach(() => {
usbCommands = new UsbCommands({
access_token: '1234',
apiUrl: 'https://api.particle.io'
});
});

it('formats output with application variables only', () => {
const result = {
env: {
FOO: { value: 'bar', isApp: true },
TEST: { value: 'baz', isApp: true }
}
};

const output = usbCommands._formatEnvOutput(result, 'P2', '0123456789abcdef');
const cleanOutput = output.map(stripAnsi);

expect(cleanOutput).to.deep.equal([
'',
'Device: 0123456789abcdef (P2)',
`${os.EOL}Environment Variables:`,
' Firmware:',
' FOO=bar',
' TEST=baz',
''
]);
Comment on lines +137 to +145
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These unit tests currently assert the header line as ${os.EOL}Environment Variables: (i.e., a single array element containing a leading newline). Because _formatEnvOutput() is consumed by iterating output.forEach(line => console.log(line)), a newline embedded inside a single element is likely unintended and will print as multiple terminal lines from one console.log call. If you change the formatter to emit a blank line as its own element and then 'Environment Variables:' as a separate element, update these expectations accordingly and you can remove the os import from this test file too.

Copilot uses AI. Check for mistakes.
});

it('formats output with system variables only', () => {
const result = {
env: {
SYS_VAR1: { value: 'value1', isApp: false },
SYS_VAR2: { value: 'value2', isApp: false }
}
};

const output = usbCommands._formatEnvOutput(result, 'P2', '0123456789abcdef');
const cleanOutput = output.map(stripAnsi);

expect(cleanOutput).to.deep.equal([
'',
'Device: 0123456789abcdef (P2)',
`${os.EOL}Environment Variables:`,
' Cloud:',
' SYS_VAR1=value1',
' SYS_VAR2=value2',
''
]);
});

it('formats output with both application and system variables', () => {
const result = {
env: {
APP_KEY: { value: 'app_value', isApp: true },
SYS_KEY: { value: 'sys_value', isApp: false },
ANOTHER_APP: { value: 'another_app', isApp: true }
}
};

const output = usbCommands._formatEnvOutput(result, 'Photon', 'abc123def456');
const cleanOutput = output.map(stripAnsi);

expect(cleanOutput).to.deep.equal([
'',
'Device: abc123def456 (Photon)',
`${os.EOL}Environment Variables:`,
' Firmware:',
' ANOTHER_APP=another_app',
' APP_KEY=app_value',
'',
' Cloud:',
' SYS_KEY=sys_value',
''
]);
});

it('formats output when no environment variables are set', () => {
const result = {
env: {}
};

const output = usbCommands._formatEnvOutput(result, 'Argon', 'device123');
const cleanOutput = output.map(stripAnsi);

expect(cleanOutput).to.deep.equal([
'',
'Device: device123 (Argon)',
' No environment variables set',
''
]);
});

it('includes snapshot hash when present', () => {
const result = {
env: {
MY_VAR: { value: 'my_value', isApp: true }
},
snapshot: {
hash: 'abc123def456789'
}
};

const output = usbCommands._formatEnvOutput(result, 'P2', '0123456789abcdef');
const cleanOutput = output.map(stripAnsi);

expect(cleanOutput).to.deep.equal([
'',
'Device: 0123456789abcdef (P2)',
'Snapshot Hash: abc123def456789',
`${os.EOL}Environment Variables:`,
' Firmware:',
' MY_VAR=my_value',
''
]);
});

it('sorts variables alphabetically within each category', () => {
const result = {
env: {
ZEBRA: { value: 'z', isApp: true },
APPLE: { value: 'a', isApp: true },
BANANA: { value: 'b', isApp: true },
SYS_Z: { value: 'sz', isApp: false },
SYS_A: { value: 'sa', isApp: false }
}
};

const output = usbCommands._formatEnvOutput(result, 'P2', 'device123');
const cleanOutput = output.map(stripAnsi);

expect(cleanOutput).to.deep.equal([
'',
'Device: device123 (P2)',
`${os.EOL}Environment Variables:`,
' Firmware:',
' APPLE=a',
' BANANA=b',
' ZEBRA=z',
'',
' Cloud:',
' SYS_A=sa',
' SYS_Z=sz',
''
]);
});

it('handles special characters in values', () => {
const result = {
env: {
SPECIAL: { value: 'value with spaces & symbols!@#$%', isApp: true }
}
};

const output = usbCommands._formatEnvOutput(result, 'P2', 'device123');
const cleanOutput = output.map(stripAnsi);

expect(cleanOutput).to.deep.equal([
'',
'Device: device123 (P2)',
`${os.EOL}Environment Variables:`,
' Firmware:',
' SPECIAL=value with spaces & symbols!@#$%',
''
]);
});
});
});
2 changes: 1 addition & 1 deletion test/e2e/help.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ describe('Help & Unknown Command / Argument Handling', () => {
'token revoke', 'token create', 'token', 'udp send', 'udp listen', 'udp', 'update',
'update-cli', 'usb list', 'usb start-listening', 'usb listen',
'usb stop-listening', 'usb safe-mode', 'usb dfu', 'usb reset',
'usb setup-done', 'usb configure', 'usb cloud-status', 'usb network-interfaces', 'usb',
'usb setup-done', 'usb configure', 'usb cloud-status', 'usb network-interfaces', 'usb env', 'usb',
'variable list', 'variable get', 'variable monitor', 'variable',
'webhook create', 'webhook list', 'webhook delete', 'webhook POST',
'webhook GET', 'webhook', 'whoami', 'wifi add', 'wifi join', 'wifi clear', 'wifi list', 'wifi remove', 'wifi current', 'wifi'
Expand Down
Loading