Co-authored-by: Rahul Chaudhary <7551493+rahulch95@users.noreply.github.com>
This commit is contained in:
Jeff Principe 2018-09-06 16:07:49 -07:00
Родитель a08d057cf9
Коммит df3c004c4d
30 изменённых файлов: 5966 добавлений и 49 удалений

17
.github/ISSUE_TEMPLATE/bug_report.md поставляемый Normal file
Просмотреть файл

@ -0,0 +1,17 @@
---
name: Bug report
about: Create a bug report
---
<!-- ⚠️ Please search existing issues to avoid creating duplicates. ⚠️ -->
<!-- Describe the bug here. -->
## Details
- Operating System type and version:
- Output of `iotc-explorer --version`:
- Output of `node -v`:
### Steps to reproduce
1.
2.

7
.github/ISSUE_TEMPLATE/feature_request.md поставляемый Normal file
Просмотреть файл

@ -0,0 +1,7 @@
---
name: Feature request
about: Suggest a feature or enhancement
---
<!-- ⚠️ Please search existing issues to avoid creating duplicates. ⚠️ -->
<!-- Describe the enhancement here. -->

7
.github/ISSUE_TEMPLATE/question.md поставляемый Normal file
Просмотреть файл

@ -0,0 +1,7 @@
---
name: Question
about: Ask a question
---
<!-- ⚠️ Please search existing issues to avoid creating duplicates. ⚠️ -->
<!-- Describe your question here. -->

13
.github/PULL_REQUEST_TEMPLATE.md поставляемый Normal file
Просмотреть файл

@ -0,0 +1,13 @@
<!--
Thank you for your pull request. Please provide a description above and review
the requirements below.
Contributors guide: https://github.com/Azure/iotc-explorer/blob/master/README.md#contributing
-->
### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->
- [ ] `npm test` passes
- [ ] documentation is changed or added
- [ ] commit message follows [commit guidelines](https://github.com/Azure/iotc-explorer/blob/master/README.md#committing)

41
.gitignore поставляемый
Просмотреть файл

@ -1,15 +1,6 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
@ -20,42 +11,14 @@ coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
# Build output
dist

11
.npmignore Normal file
Просмотреть файл

@ -0,0 +1,11 @@
node_modules/
.nyc_output/
.vscode/
.github/
coverage/
src/
dist/**/*.spec.*
*.tgz
npm-debug.log*

26
.travis.yml Normal file
Просмотреть файл

@ -0,0 +1,26 @@
language: node_js
cache:
directories:
# https://twitter.com/maybekatz/status/905213355748720640
- ~/.npm
node_js:
- '8'
- '10'
# Trigger a push build on master branch + PRs build on every branch. Avoid
# double build on PRs (See https://github.com/travis-ci/travis-ci/issues/1147)
branches:
only:
- master
stages:
- commitlint
- test
jobs:
include:
- stage: commitlint
node_js: '8'
script: commitlint-travis
- stage: test
script: npm run build-verify

200
README.md
Просмотреть файл

@ -1,14 +1,194 @@
# iotc-explorer
# Contributing
Command-line interface for interacting with Azure IoT Central devices and
applications.
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.microsoft.com.
## Installing
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
To use `iotc-explorer`, you will need to have the following installed:
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
* Node.js version 8.x or higher - https://nodejs.org
Once you have the prerequisites installed, run the following from your command
line to install:
```
npm install -g iotc-explorer
```
*NOTE: You will typically need to run the install command with `sudo` in
Unix-like environments.*
Once installed, you can run `iotc-explorer --help` to verify everything is
working and get an overview of the available commands:
```
$ iotc-explorer --help
iotc-explorer <command>
Commands:
iotc-explorer config Manage configuration values for the CLI
iotc-explorer get-twin <deviceId> Get the IoT Hub device twin for a specific device
iotc-explorer login [token] Log in to an Azure IoT Central application
iotc-explorer monitor-messages [deviceId] Monitor messages being sent to a specific device (if
device id is provided), or all devices
Options:
--version Show version number [boolean]
--help Show help [boolean]
```
## Running `iotc-explorer`
Below are some commands and common options that you can run when using
`iotc-explorer`. To view the full set of commands and options, you can pass
`--help` to `iotc-explorer` or any of its subcommands.
### Login
Before you get going, you will want to have one of your IoT Central application
administrators get a SAS token for you to use. You can then use that token to
log in to the CLI by running:
```sh
iotc-explorer login "SharedAccessSignature sr=<your-resource>&sig=<your-signature>&skn=<your-key-name>&se=<your-expiry>"
```
If you would rather not have the token persisted in your shell history, you can
leave the token out and instead provide it when prompted:
```
iotc-explorer login
```
### Monitor Device Messages
You can watch the messages coming from either a specific device or all devices
in your application using the `monitor-messages` command. This will start a
watcher that will continuously output new messages as they come in.
To watch all devices in your application, simply run:
```
iotc-explorer monitor-messages
```
To watch a specific device, just add the device's id to the end of the command:
```
iotc-explorer monitor-messages <your-device-id>
```
You can also have the command output a more machine-friendly format by adding
the `--raw` option to the command:
```
iotc-explorer monitor-messages --raw
```
### Get Device Twin
You can use the `get-twin` command to get the contents of the twin for an IoT
Central device. To do so, simply run the following:
```
iotc-explorer get-twin <your-device-id>
```
As with `monitor-messages`, you can get a more machine-friendly output by
passing the `--raw` option:
```
iotc-explorer get-twin <your-device-id> --raw
```
## Contributing
### Developer Setup
For your first time setup, make sure you've done the following:
1. Make sure you have the [prerequisites](#installing) installed.
2. Clone this repository to wherever you want to develop.
3. Run `cd iotc-explorer` to enter the repository folder.
4. Run `npm install`, then `npm run build` to get things configured.
### Writing Code
Once you're ready to start changing code, it is recommended that you link your
project to the `iotc-explorer` executable by running the following (may require
`sudo`):
```
npm link
```
Now, when you run `iotc-explorer`, it will point to the code in your development
folder. To make the executable reflect your changes as they're made, set up a
watch task in a terminal window to the side:
```
npm run watch
```
Now, whenever you make edits to the code you will be able to use them by running
the `iotc-explorer` command on your machine.
When you're ready to stop local development, you can remove your connection to
the `iotc-explorer` executable by running the following (may require `sudo`):
```
npm unlink
```
### Committing
This project uses the [Angular commit style][angular commit style] for
generating changelogs and determining release versions. Any pull request with
commits that don't follow this style will fail continuous integration. If you're
not familiar with the style, you can run the following instead of the standard
`git commit` to get a guided walkthrough to generating your commit message:
```
npm run commit
```
### Releasing
When it's time to cut a new release, run the following from the repository
folder. This will (1) fetch the latest updates, (2) automatically update the
package version and the changelog, (3) publish the package and (4) push the
changes back into the repository:
```
git checkout master
git pull
npm run build-verify
npm run release
npm publish
git push
```
### Contributor License Agreement
This project welcomes contributions and suggestions. Most contributions require
you to agree to a Contributor License Agreement (CLA) declaring that you have
the right to, and actually do, grant us the rights to use your contribution. For
details, visit https://cla.microsoft.com.
When you submit a pull request, a CLA-bot will automatically determine whether
you need to provide a CLA and decorate the PR appropriately (e.g., label,
comment). Simply follow the instructions provided by the bot. You will only need
to do this once across all repos using our CLA.
### Code of Conduct
This project has adopted the [Microsoft Open Source Code of Conduct][]. For more
information see the [Code of Conduct FAQ][] or contact
[opencode@microsoft.com][] with any additional questions or comments.
[Microsoft Open Source Code of Conduct]: https://opensource.microsoft.com/codeofconduct/
[Code of Conduct FAQ]: https://opensource.microsoft.com/codeofconduct/faq/
[opencode@microsoft.com]: mailto:opencode@microsoft.com
[angular commit style]: https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-guidelines

4643
package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

66
package.json Normal file
Просмотреть файл

@ -0,0 +1,66 @@
{
"name": "iotc-explorer",
"description": "CLI for interacting with Azure IoT Central devices and applications",
"version": "1.0.0",
"author": "Microsoft",
"license": "MIT",
"main": "./dist/iotc-explorer.js",
"engines": {
"node": ">=8.0.0"
},
"bin": {
"iotc-explorer": "dist/iotc-explorer.js"
},
"keywords": [
"iotcentral",
"iot",
"azure",
"device",
"monitor"
],
"scripts": {
"commit": "commit",
"commitmsg": "commitlint -e $GIT_PARAMS",
"clean": "rimraf dist",
"lint": "tslint --project tsconfig.json -e \"**/*.json\" --fix",
"prebuild": "npm run clean && npm run lint",
"build": "tsc",
"build-verify": "npm run clean && tslint --project tsconfig.json -e \"**/*.json\" && tsc",
"test": "npm run build-verify",
"prepare": "npm t",
"watch": "tsc -w",
"start": "./dist/iotc-explorer.js",
"release": "standard-version"
},
"dependencies": {
"axios": "^0.18.0",
"azure-event-hubs": "^0.2.6",
"azure-iothub": "^1.7.3",
"conf": "^2.0.0",
"get-caller-file": "^2.0.0",
"inquirer": "^6.2.0",
"prettyjson": "^1.2.1",
"source-map-support": "^0.5.6",
"string-width": "^2.1.1",
"yargs": "^12.0.1"
},
"devDependencies": {
"@commitlint/cli": "^7.1.2",
"@commitlint/config-conventional": "^7.1.2",
"@commitlint/prompt-cli": "^7.1.2",
"@commitlint/travis-cli": "^7.1.2",
"@types/async-lock": "^1.1.0",
"@types/conf": "^1.4.0",
"@types/inquirer": "0.0.43",
"@types/node": "^8.10.29",
"@types/prettyjson": "0.0.28",
"@types/string-width": "^2.0.0",
"@types/yargs": "^11.1.1",
"husky": "^0.14.3",
"rimraf": "^2.6.2",
"standard-version": "^4.4.0",
"tslint": "^5.11.0",
"tslint-consistent-codestyle": "^1.13.3",
"typescript": "^3.0.1"
}
}

12
src/commands/config.ts Normal file
Просмотреть файл

@ -0,0 +1,12 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { group } from '../core/command';
import * as resources from '../resources.json';
export = group(
'config',
resources.commands.config.description
);

Просмотреть файл

@ -0,0 +1,18 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import command from '../../core/command';
import * as config from '../../core/config';
import * as resources from '../../resources.json';
export = command<{ key: string; }>({
command: 'delete <key>',
describe: resources.commands.config.commands.delete.description,
handler(args) {
config.del(args.key as config.ConfigKey);
}
});

Просмотреть файл

@ -0,0 +1,18 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import command from '../../core/command';
import * as config from '../../core/config';
import * as resources from '../../resources.json';
export = command<{ key: string }>({
command: 'get <key>',
describe: resources.commands.config.commands.get.description,
handler(args, log) {
const value = config.getOverride(args.key as config.ConfigKey);
log.info(typeof value === 'string' ? value : JSON.stringify(value));
}
});

Просмотреть файл

@ -0,0 +1,17 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import command from '../../core/command';
import * as config from '../../core/config';
import * as resources from '../../resources.json';
export = command({
command: 'list',
describe: resources.commands.config.commands.list.description,
handler(args, log) {
log.json(config.list());
}
});

Просмотреть файл

@ -0,0 +1,24 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import command from '../../core/command';
import * as config from '../../core/config';
import * as resources from '../../resources.json';
export = command<{ key: string; value: string; }>({
command: 'set <key> <value>',
describe: resources.commands.config.commands.set.description,
handler(args) {
let value: any;
try {
value = JSON.parse(args.value);
} catch {
value = args.value;
}
config.set(args.key as config.ConfigKey, value);
}
});

72
src/commands/get-twin.ts Normal file
Просмотреть файл

@ -0,0 +1,72 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Registry } from 'azure-iothub';
import * as util from 'util';
import * as api from '../core/api';
import command from '../core/command';
import CliError from '../core/error';
import * as opts from '../core/options';
import * as resources from '../resources.json';
export = command<{ deviceId: string }, { 'hide-metadata': boolean } & opts.raw>({
command: 'get-twin <deviceId>',
describe: resources.commands.getTwin.description,
options: {
'hide-metadata': {
describe: resources.commands.getTwin.options.hideMetadata,
alias: 'h',
type: 'boolean',
default: false
},
...opts.raw
},
async handler(args, log) {
const sasTokens = await api.generateSasTokens();
const iothubRegistry = Registry.fromSharedAccessSignature(sasTokens.iothubTenantSasToken.sasToken);
let twin: any;
try {
twin = await util.promisify(iothubRegistry.getTwin.bind(iothubRegistry))(args.deviceId);
} catch (e) {
switch (e && e.name) {
// The device was not found.
case 'DeviceNotFoundError':
throw new CliError(
'DEVICE_NOT_FOUND',
util.format(resources.errors.iotHub.deviceNotFound, args.deviceId)
);
// We just group other errors together for now since we don't
// expect to see any of them.
default:
throw new CliError(
'IOTHUB_REGISTRY_ERROR',
util.format(resources.errors.iotHub.registryError, e && (e.name || e.message))
);
}
}
// We don't want internal information to be displayed
delete twin._registry;
// If the user does not want metadata, delete $metadata and $version
if (args['hide-metadata'] && twin.properties) {
if (twin.properties.desired) {
delete twin.properties.desired.$metadata;
delete twin.properties.desired.$version;
}
if (twin.properties.reported) {
delete twin.properties.reported.$metadata;
delete twin.properties.reported.$version;
}
}
log.json(twin);
}
});

95
src/commands/login.ts Normal file
Просмотреть файл

@ -0,0 +1,95 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import * as inquirer from 'inquirer';
import * as querystring from 'querystring';
import * as util from 'util';
import command from '../core/command';
import * as config from '../core/config';
import { SAS_TOKEN_PREFIX } from '../core/constants';
import CliError from '../core/error';
import * as resources from '../resources.json';
export = command<{ token?: string }>({
command: 'login [token]',
describe: resources.commands.login.description,
async handler(args, log) {
let inputToken: string;
if (args.token) {
// The token was provided as an option to the command. Just use it
// directly
inputToken = args.token;
} else {
// Prompt the user for their token and use that value.
inputToken = (await inquirer.prompt<{ token: string }>({
type: 'input',
name: 'token',
message: 'SAS Token',
prefix: '',
suffix: ':'
})).token;
}
const invalidSasTokenCode = 'INVALID_SAS_TOKEN';
// Tokens must start with "SharedAccessSignature "
if (!inputToken.startsWith(SAS_TOKEN_PREFIX)) {
throw new CliError(
invalidSasTokenCode,
resources.errors.sasToken.invalidPrefix
);
}
const token = inputToken.substring(SAS_TOKEN_PREFIX.length);
// If the token isn't a valid uri fragment, this can throw. Catch it and
// indicate that the token is malformed
let parsed: querystring.ParsedUrlQuery;
try {
parsed = querystring.parse(token);
} catch {
throw new CliError(
invalidSasTokenCode,
resources.errors.sasToken.malformed
);
}
// The token must have the sr, se, and skn properties to be valid
if (!parsed || !parsed.sr || !parsed.se || !parsed.skn) {
throw new CliError(
invalidSasTokenCode,
resources.errors.sasToken.invalid
);
}
// If there is a token expiry, it cannot be in the past
if (parsed.se && Number(parsed.se) < Date.now()) {
throw new CliError(
invalidSasTokenCode,
resources.errors.sasToken.expired
);
}
// Set the relevant information in the configuration store
config.set('iotc.credentials.token', inputToken);
config.set('iotc.credentials.application', parsed.sr);
// Delete any cached hub credentials we have as they may be pointing to
// the wrong place
config.del('iotc.credentials.hubs');
// Inform the user
log.info(
parsed.se
? util.format(
resources.commands.login.logs.successWithExpiry,
new Date(Number(parsed.se)).toString()
)
: resources.commands.login.logs.successNoExpiry
);
}
});

Просмотреть файл

@ -0,0 +1,99 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { EventHubClient, TokenType } from 'azure-event-hubs';
import * as util from 'util';
import * as api from '../core/api';
import command from '../core/command';
import * as opts from '../core/options';
import * as resources from '../resources.json';
export = command<{ deviceId?: string; }, { 'start-time': number } & opts.raw>({
command: 'monitor-messages [deviceId]',
describe: resources.commands.monitorMessages.description,
options: {
'start-time': {
describe: resources.commands.monitorMessages.options.startTime,
type: 'number',
default: Date.now()
},
...opts.raw
},
async handler(args, log): Promise<void> {
const sasTokens = await api.generateSasTokens();
const eventhubClient = EventHubClient.createFromTokenProvider(
sasTokens.eventhubSasToken.hostname.substring(5),
sasTokens.eventhubSasToken.entityPath,
{
tokenRenewalMarginInSeconds: -1,
tokenValidTimeInSeconds: 3600,
getToken: () => Promise.resolve({
tokenType: TokenType.CbsTokenTypeSas,
token: sasTokens.eventhubSasToken.sasToken,
expiry: sasTokens.expiry,
}),
},
);
const partitionIds = await eventhubClient.getPartitionIds();
const deviceId = args.deviceId;
const startTime = args['start-time'];
// Once we get here we know we can set up the client and start receiving
// messages.
log.info(
deviceId
? util.format(resources.commands.monitorMessages.logs.monitoringDevice, deviceId)
: resources.commands.monitorMessages.logs.monitoringAll
);
log.blank();
partitionIds.map(partitionId => {
eventhubClient.receive(
partitionId,
message => {
const messageDeviceId = message.annotations && message.annotations['iothub-connection-device-id'];
// If device id is provided, and the message is not sent to it, return
const isDeviceRelevant = !deviceId || messageDeviceId === deviceId;
// If message time is before the specified start time, return
const messageTime = message.annotations && message.annotations['x-opt-enqueued-time'] || Date.now();
const isMessageBeforeStartTime = messageTime < startTime;
// We don't want messages that don't fit our conditions
if (!isDeviceRelevant || isMessageBeforeStartTime) {
return;
}
const dateString = new Date(messageTime).toUTCString();
// Log the metadata and the message body
log.divider(messageDeviceId
? util.format(
resources.commands.monitorMessages.logs.messageHeaderKnownDevice,
messageDeviceId,
dateString
)
: util.format(
resources.commands.monitorMessages.logs.messageHeaderUnknownDevice,
dateString
)
);
log.json(message.body);
log.blank();
},
error => {
// Errors here should be fatal. Log it and exit
log.error(error);
process.exit(1);
}
);
});
}
});

90
src/core/api.ts Normal file
Просмотреть файл

@ -0,0 +1,90 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import ax, { AxiosError } from 'axios';
import * as https from 'https';
import * as util from 'util';
import * as resources from '../resources.json';
import * as config from './config';
import CliError from './error';
export type IotHubSasTokens = {
iothubTenantSasToken: {
sasToken: string,
},
eventhubSasToken: {
sasToken: string,
entityPath: string,
hostname: string,
},
expiry: number,
};
const axios = ax.create({
baseURL: `https://${config.get('iotc.api.host')}/${config.get('iotc.api.version')}/`,
httpsAgent: new https.Agent({
keepAlive: true,
rejectUnauthorized: !!config.get('core.rejectUnauthorized')
}),
responseType: 'json'
});
export async function generateSasTokens(): Promise<IotHubSasTokens> {
const { token, appId } = getContext();
const cacheTtl = config.get('core.tokenCacheTtl') as number;
const cachedTokens = config.get('iotc.credentials.hubs') as any;
// If we have cached tokens and a nonzero ttl less than the time to expiry,
// use the cached token.
if (cacheTtl && cachedTokens && (cachedTokens.expiry * 1000 - Date.now()) > cacheTtl) {
return cachedTokens;
}
try {
const response = await axios.post<IotHubSasTokens>(
`/applications/${appId}/diagnostics/sasTokens`,
undefined,
{
headers: {
Authorization: token
}
}
);
config.set('iotc.credentials.hubs', response.data);
return response.data;
} catch (e) {
throw new CliError(
'IOTC_API_ERROR',
util.format(resources.errors.iotCentral.genericError, getErrorMessage(e))
);
}
}
function getContext() {
const token = config.get('iotc.credentials.token') as string | undefined;
const appId = config.get('iotc.credentials.application') as string | undefined;
// if iot central token does not exist, throw error
if (!token || !appId) {
throw new CliError(
'INVALID_CREDENTIALS',
resources.errors.iotCentral.invalidCredentials
);
}
return { token, appId };
}
function getErrorMessage(err: AxiosError) {
const responseCode = err &&
err.response &&
err.response.data &&
err.response.data.error &&
err.response.data.error.message;
const errorCode = err.message;
return responseCode || errorCode || 'unknown error';
}

121
src/core/command.ts Normal file
Просмотреть файл

@ -0,0 +1,121 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import * as path from 'path';
import * as yargs from 'yargs';
import Log from './log';
import { Arguments, ArgvNoOptions, OptionConfig } from './types';
export interface Command<P extends object, O extends object> {
/**
* Command name and positional arguments as specified to yargs.
*/
command: string;
/**
* Optional aliases that can be used instead of the main command name.
*/
aliases?: string[];
/**
* Description of the command functionality and behavior.
*/
describe: string;
/**
* Map of command option names to their configurations. All configuration
* that can be passed to a yargs option is allowed, except for the
* `required` key.
*/
options?: {
[K in keyof O]: OptionConfig<O[K]>;
};
/**
* Optional builder function for specifying additional command setup. Allows
* all of the same capabilities as the yargs function of the same name
* except for setting options as those are done using the `options` key.
*/
builder?: (yargs: ArgvNoOptions) => ArgvNoOptions;
/**
* Command handler function invoked when executing this command. It may be
* synchronous or asynchronous.
*/
handler: (args: Arguments<P & O>, log: Log) => void | Promise<void>;
}
/**
* Register a command with the given configuration parameters in the CLI.
*
* @param command Command configuration object.
*/
export default function command<P extends object = {}, O extends object = {}>(command: Command<P, O>) {
return {
command: command.command,
aliases: Array.isArray(command.aliases) && command.aliases.length > 0 ? command.aliases : undefined,
describe: command.describe,
builder(yargs: yargs.Argv) {
if (command.builder) {
yargs = command.builder(yargs) as yargs.Argv;
}
if (command.options) {
yargs = yargs.options(command.options);
}
return yargs;
},
async handler(args: Arguments<P & O>) {
const log = new Log(args);
const rejectionHandler = (err: any) => {
log.error(err);
process.exit(1);
};
process.on('unhandledRejection', rejectionHandler);
try {
await command.handler(args, log);
} catch (e) {
rejectionHandler(e);
} finally {
process.removeListener('unhandledRejection', rejectionHandler);
}
}
};
}
/**
* Create a command group with the given name(s) and description. All commands
* created in a sibling directory with the same name as the group will be
* available as part of that group.
*
* @param cmd The name of the command group as it appears in the command
* line. Can also be specified as an array, where entries after
* the first one are aliases for the command group. Please note
* that the expected folder name (as well as the name that
* appears in the help text) is always the first entry in the
* array.
* @param describe The description of the command group that appears in the help
* text.
*/
export function group(cmd: string | string[], describe: string) {
const callerDir = path.dirname(require('get-caller-file')());
const relativeToCaller = path.relative(__dirname, callerDir);
const mainCommand = Array.isArray(cmd) ? cmd[0] : cmd;
return command({
command: mainCommand,
aliases: Array.isArray(cmd) ? cmd.slice(1) : [],
describe,
builder(yargs) {
return yargs
.demandCommand(1)
.commandDir(path.join(relativeToCaller, mainCommand));
},
handler: () => { }
});
}

54
src/core/config.ts Normal file
Просмотреть файл

@ -0,0 +1,54 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import Conf = require('conf');
import { CONFIG_FOLDER } from './constants';
export type ConfigKey =
'core.rejectUnauthorized' |
'core.tokenCacheTtl' |
'iotc.api.host' |
'iotc.api.version' |
'iotc.credentials.token' |
'iotc.credentials.application' |
'iotc.credentials.hubs';
const defaults: { [K in ConfigKey]?: unknown } = {
'core.rejectUnauthorized': false,
'core.tokenCacheTtl': 10 * 60 * 1000, // 10 minutes
'iotc.api.host': 'api.azureiotcentral.com',
'iotc.api.version': 'v1-beta'
};
// Default configuration values. We keep these out of the defaults provided to
// conf because if we ever have to change the defaults, they will not be
// respected.
const conf = new Conf<any>({ projectName: CONFIG_FOLDER });
export function get(key: ConfigKey): unknown {
return conf.has(key) ? conf.get(key) : defaults[key];
}
export function getOverride(key: ConfigKey): unknown {
return conf.get(key);
}
export function getDefault(key: ConfigKey): unknown {
return defaults[key];
}
export function set(key: ConfigKey, value: any) {
return conf.set(key, value);
}
export function list(): unknown {
return conf.store;
}
export function del(key: ConfigKey) {
conf.delete(key);
}

11
src/core/constants.ts Normal file
Просмотреть файл

@ -0,0 +1,11 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export const SAS_TOKEN_PREFIX = 'SharedAccessSignature ';
export const CONFIG_FOLDER = 'iotc-explorer';
export const DEFAULT_CLI_WIDTH = 80;
export const DIVIDER_CHAR = '=';
export const MIN_DIVIDER_PAD = 5;

13
src/core/error.ts Normal file
Просмотреть файл

@ -0,0 +1,13 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export default class CliError extends Error {
code: string;
constructor(code: string, message: string) {
super(message);
this.code = code;
}
}

73
src/core/log.ts Normal file
Просмотреть файл

@ -0,0 +1,73 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import * as prettyjson from 'prettyjson';
import stringWidth = require('string-width');
import * as util from 'util';
import * as resources from '../resources.json';
import { DEFAULT_CLI_WIDTH, DIVIDER_CHAR, MIN_DIVIDER_PAD } from './constants';
import CliError from './error';
import * as opts from './options';
export default class Log {
private _raw: boolean;
constructor(options?: Partial<opts.raw>) {
this._raw = !!(options && options.raw);
}
info(message: string) {
if (!this._raw) {
console.log(message);
}
}
error(error: any) {
if (error instanceof CliError) {
console.error(util.format(resources.general.knownError, error.message, error.code));
} else {
console.error(util.format(resources.general.unknownError, error && error.message || error));
}
}
json(value: any) {
if (this._raw) {
console.log(JSON.stringify(value));
} else {
console.log(prettyjson.render(value));
}
}
divider(message?: string) {
if (this._raw) {
return;
}
const cliWidth = typeof (process.stdout as any).getWindowSize === 'function'
? (process.stdout as any).getWindowSize()[0]
: DEFAULT_CLI_WIDTH;
if (!message) {
console.log(DIVIDER_CHAR.repeat(cliWidth));
} else {
const messageWidth = stringWidth(message);
const averagePad = Math.max(MIN_DIVIDER_PAD, (cliWidth - messageWidth) / 2);
// If there is an odd difference between the widths, we give the extra pad to the left
const leftPad = Math.ceil(averagePad);
const rightPad = Math.floor(averagePad);
console.log(`${DIVIDER_CHAR.repeat(leftPad - 1)} ${message} ${DIVIDER_CHAR.repeat(rightPad - 1)}`);
}
}
blank() {
if (!this._raw) {
console.log();
}
}
}

22
src/core/options.ts Normal file
Просмотреть файл

@ -0,0 +1,22 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import * as resources from '../resources.json';
import { OptionConfig } from './types';
export type Options<T extends object> = {
[K in keyof T]: OptionConfig<T[K]>
};
export type raw = { raw: boolean; };
export const raw: Options<raw> = {
raw: {
alias: 'r',
default: false,
describe: resources.options.raw,
type: 'boolean'
}
};

20
src/core/types.ts Normal file
Просмотреть файл

@ -0,0 +1,20 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import * as yargs from 'yargs';
export type OptionType<T> = T extends any[] ? 'array'
: T extends string ? 'string'
: T extends number ? 'number' | 'count'
: T extends boolean ? 'boolean'
: NonNullable<yargs.Options['type']>;
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export type ArgvNoOptions = Omit<yargs.Argv, 'option' | 'options'>;
export type Arguments<T extends object> = Pick<yargs.Arguments, '_' | '$0'> & T;
export type OptionConfig<T> = Omit<yargs.Options, 'required' | 'require'> & { type: OptionType<T> };

23
src/iotc-explorer.ts Normal file
Просмотреть файл

@ -0,0 +1,23 @@
#! /usr/bin/env node
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import * as yargs from 'yargs';
require('source-map-support').install();
function main(): yargs.Argv {
return yargs
.locale('en')
.commandDir('commands')
.help()
.demandCommand(1)
.strict()
.wrap(yargs.terminalWidth());
}
// tslint:disable-next-line:no-unused-expression
main().argv;

72
src/resources.json Normal file
Просмотреть файл

@ -0,0 +1,72 @@
{
"errors": {
"sasToken": {
"invalidPrefix": "Provided token is not a SharedAccessSignature token. Please provide a valid token",
"malformed": "Provided SAS token is malformed. Please provide a valid token",
"invalid": "Provided SAS token is invalid. Please provide a valid token",
"expired": "Provided SAS token is expired. Please provide a valid token"
},
"iotHub": {
"deviceNotFound": "Could not find twin for device with id %s",
"registryError": "Failed to fetch twin from IoT Hub with error '%s'"
},
"iotCentral": {
"invalidCredentials": "Invalid credentials. Please login and then retry this action",
"genericError": "IoT Central API call failed with message %s"
}
},
"general": {
"knownError": "Error: %s [%s]",
"unknownError": "Unknown Error: %s"
},
"commands": {
"login": {
"description": "Log in to an Azure IoT Central application",
"prompts": {
"token": "SAS Token"
},
"logs": {
"successWithExpiry": "Login successful. Session expires %s",
"successNoExpiry": "Login successful."
}
},
"getTwin": {
"description": "Get the IoT Hub device twin for a specific device",
"options": {
"hideMetadata": "Hide all device twin metadata"
}
},
"monitorMessages": {
"description": "Monitor messages being sent to a specific device (if device id is provided), or all devices",
"options": {
"startTime": "Only messages sent after start-time will be displayed. Defaults to current unix ms epoch time"
},
"logs": {
"monitoringDevice": "Monitoring messages from device %s...",
"monitoringAll": "Monitoring messages from all devices...",
"messageHeaderKnownDevice": "Device %s at %s",
"messageHeaderUnknownDevice": "Unknown device at %s"
}
},
"config": {
"description": "Manage configuration values for the CLI",
"commands": {
"get": {
"description": "Retrieve the value for the requested configuration key"
},
"set": {
"description": "Set the specified key to the given value"
},
"list": {
"description": "List all configuration keys and values"
},
"delete": {
"description": "Delete the specified configuration key, reverting to the default value"
}
}
}
},
"options": {
"raw": "Return the raw machine-readable output instead of pretty-printed output"
}
}

21
tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"strict": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"suppressImplicitAnyIndexErrors": true,
"forceConsistentCasingInFileNames": true,
"newLine": "lf",
"rootDir": "src",
"outDir": "dist",
"preserveWatchOutput": true,
"resolveJsonModule": true
},
"include": [
"src"
]
}

109
tslint.json Normal file
Просмотреть файл

@ -0,0 +1,109 @@
{
"rulesDirectory": ["tslint-consistent-codestyle"],
"rules": {
"align": [
true,
"parameters"
],
"array-type": [
true,
"array"
],
"class-name": true,
"curly": true,
"comment-format": [
true,
"check-space"
],
"file-header": [
true,
"^!\\s*\\* Copyright \\(c\\) Microsoft Corporation\\. All rights reserved\\.\n \\* Licensed under the MIT License\\.\\s*$",
"Copyright (c) Microsoft Corporation. All rights reserved.\nLicensed under the MIT License."
],
"import-spacing": true,
"indent": [
true,
"spaces"
],
"max-line-length": [true, 120],
"new-parens": true,
"no-construct": true,
"no-duplicate-variable": true,
"no-eval": true,
"no-internal-module": true,
"no-invalid-this": true,
"no-misused-new": true,
"no-trailing-whitespace": true,
"no-unused-expression": true,
"no-var-keyword": true,
"object-literal-key-quotes": [
true,
"as-needed"
],
"one-line": [
true,
"check-catch",
"check-else",
"check-finally",
"check-open-brace",
"check-whitespace"
],
"only-arrow-functions": [
true,
"allow-declarations"
],
"ordered-imports": [
true,
{
"grouped-imports": true
}
],
"prefer-const": true,
"prefer-for-of": true,
"quotemark": [
true,
"single"
],
"semicolon": [
true,
"always"
],
"space-before-function-paren": [
true,
{
"anonymous": "always",
"named": "never",
"asyncArrow": "always"
}
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"variable-name": [
true,
"ban-keywords"
],
"whitespace": [
true,
"check-branch",
"check-decl",
"check-module",
"check-operator",
"check-separator",
"check-type",
"check-typecast",
"check-preblock"
]
}
}