Skip to content

Commit

Permalink
feat(cli): doctor command (#10180)
Browse files Browse the repository at this point in the history
* feat: cli doctor command

* feat: 3 checks

* feat: doctor

* feat: doctor

* test: ut

* fix: string

* test: ut

* fix: string

* fix: string

* test: ut

* test: ut
  • Loading branch information
jayzhang authored Oct 25, 2023
1 parent 4f8f7a0 commit bb3771d
Show file tree
Hide file tree
Showing 11 changed files with 397 additions and 10 deletions.
2 changes: 1 addition & 1 deletion packages/cli/src/cmds/preview/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export const doctorResult = {
SideLoadingDisabled:
"Your Microsoft 365 tenant admin hasn't enabled sideloading permission for your account. You can't install your app to Teams!",
NotSignIn: "No Microsoft 365 account login",
SignInSuccess: `Microsoft 365 Account (@account) is logged in and sideloading enabled`,
SignInSuccess: `Microsoft 365 Account (@account) is logged in and sideloading permission enabled`,
SkipTrustingCert: "Skip trusting development certificate for localhost",
HelpLink: `Please refer to @Link for more information.`,
NgrokWarning:
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/cmds/preview/previewEnv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ export default class PreviewEnv extends YargsCommand {
return ok(null);
}

protected async checkM365Account(appTenantId?: string): Promise<
async checkM365Account(appTenantId?: string): Promise<
Result<
{
tenantId?: string;
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/colorize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function colorize(message: string, type: TextType): string {
case TextType.Info:
return message;
case TextType.Hyperlink:
return chalk.cyanBright(message);
return chalk.cyanBright.underline(message);
case TextType.Email:
case TextType.Important:
return chalk.magentaBright(message);
Expand All @@ -41,8 +41,9 @@ export function colorize(message: string, type: TextType): string {
}
}

export const DoneText = colorize(`(${figures.tick}) Done: `, TextType.Success);
export const SuccessText = colorize(`(${figures.tick}) Success: `, TextType.Success);
export const WarningText = colorize(`(${figures.warning}) Warning: `, TextType.Success);
export const WarningText = colorize(`(${figures.warning}) Warning: `, TextType.Warning);
export const ErrorPrefix = `(${figures.cross}) Error: `;

export function replaceTemplateString(template: string, ...args: string[]): string {
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/commands/models/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { teamsappValidateCommand } from "./teamsapp/validate";
import { teamsappPackageCommand } from "./teamsapp/package";
import { teamsappPublishCommand } from "./teamsapp/publish";
import { isCliV3Enabled } from "@microsoft/teamsfx-core";
import { teamsappDoctorCommand } from "./teamsapp/doctor";

export const helpCommand: CLICommand = {
name: "help",
Expand Down Expand Up @@ -61,6 +62,7 @@ export const rootCommand: CLICommand = {
teamsappValidateCommand,
teamsappPackageCommand,
teamsappPublishCommand,
teamsappDoctorCommand,
]
: [packageCommand, updateCommand, validateCommand, publishCommand]),
],
Expand Down
152 changes: 152 additions & 0 deletions packages/cli/src/commands/models/teamsapp/doctor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { CLICommand, FxError, Result, err, ok } from "@microsoft/teamsfx-api";
import {
AppStudioScopes,
CheckerFactory,
DepsType,
EmptyLogger,
EmptyTelemetry,
FuncToolChecker,
LocalCertificateManager,
assembleError,
getSideloadingStatus,
} from "@microsoft/teamsfx-core";
import { getFxCore } from "../../../activate";
// import * as constants from "../../../cmds/preview/constants";
import * as util from "util";
import { DoneText, TextType, WarningText, colorize } from "../../../colorize";
import { signedOut } from "../../../commonlib/common/constant";
import { logger } from "../../../commonlib/logger";
import M365TokenInstance from "../../../commonlib/m365Login";
import { cliSource } from "../../../constants";
import { strings } from "../../../resource";
import { TelemetryEvent } from "../../../telemetry/cliTelemetryEvents";

export const teamsappDoctorCommand: CLICommand = {
name: "doctor",
description: "Prerequiste checker for building Microsoft Teams apps.",
options: [],
telemetry: {
event: TelemetryEvent.Doctor,
},
defaultInteractiveOption: false,
handler: async () => {
getFxCore();
const checker = new DoctorChecker();
await checker.checkAccount();
await checker.checkNodejs();
await checker.checkFuncCoreTool();
await checker.checkCert();
return ok(undefined);
},
};

export class DoctorChecker {
async checkAccount(): Promise<void> {
const res = await this.checkM365Account();
if (res.isErr()) {
logger.error(res.error.message);
} else {
logger.info(res.value);
}
}

async checkNodejs(): Promise<void> {
const nodeChecker = CheckerFactory.createChecker(
DepsType.LtsNode,
new EmptyLogger(),
new EmptyTelemetry()
);
const nodeRes = await nodeChecker.getInstallationInfo();
if (nodeRes.isInstalled) {
if (nodeRes.error) {
logger.info(
WarningText +
util.format(
strings.command.doctor.node.NotSupported,
nodeRes.details?.installVersion,
nodeRes.details?.supportedVersions.join(", ")
)
);
} else {
logger.info(
DoneText +
util.format(strings.command.doctor.node.Success, nodeRes.details?.installVersion)
);
}
} else {
logger.info(WarningText + strings.command.doctor.node.NotFound);
}
}

async checkFuncCoreTool(): Promise<void> {
const funcChecker = new FuncToolChecker();
try {
const funcRes = await funcChecker.queryFuncVersion(undefined);
logger.info(DoneText + util.format(strings.command.doctor.func.Success, funcRes.versionStr));
} catch (e) {
logger.info(WarningText + strings.command.doctor.func.NotFound);
}
}

async checkCert(): Promise<void> {
const certManager = new LocalCertificateManager();
const certRes = await certManager.setupCertificate(true, true);
if (!certRes.found) {
logger.info(WarningText + strings.command.doctor.cert.NotFound);
} else {
if (certRes.alreadyTrusted) {
logger.info(DoneText + strings.command.doctor.cert.NotFound);
} else {
logger.info(WarningText + strings.command.doctor.cert.FoundNotTrust);
}
}
}

async checkM365Account(): Promise<Result<string, FxError>> {
let result = true;
let summaryMsg = "";
let error = undefined;
let loginHint: string | undefined = undefined;
try {
let loginStatusRes = await M365TokenInstance.getStatus({ scopes: AppStudioScopes });
let token = loginStatusRes.isOk() ? loginStatusRes.value.token : undefined;
if (loginStatusRes.isOk() && loginStatusRes.value.status === signedOut) {
const tokenRes = await M365TokenInstance.getAccessToken({
scopes: AppStudioScopes,
showDialog: true,
});
token = tokenRes.isOk() ? tokenRes.value : undefined;
loginStatusRes = await M365TokenInstance.getStatus({ scopes: AppStudioScopes });
}
if (token === undefined) {
result = false;
summaryMsg = WarningText + strings.command.doctor.account.NotSignIn;
} else {
const isSideloadingEnabled = await getSideloadingStatus(token);
if (isSideloadingEnabled === false) {
// sideloading disabled
result = false;
summaryMsg = WarningText + strings.command.doctor.account.SideLoadingDisabled;
}
}
const tokenObject = loginStatusRes.isOk() ? loginStatusRes.value.accountInfo : undefined;
if (tokenObject && tokenObject.upn) {
loginHint = tokenObject.upn as string;
}
} catch (e) {
error = assembleError(e, cliSource);
return err(error);
}
if (result && loginHint) {
summaryMsg =
DoneText +
util.format(
strings.command.doctor.account.SignInSuccess,
colorize(loginHint, TextType.Email)
);
}
return ok(summaryMsg);
}
}
27 changes: 27 additions & 0 deletions packages/cli/src/resource/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,33 @@
"force": "Force upgrade the project to work with the latest version of Teams Toolkit."
},
"success": "Upgrade project successfully."
},
"doctor": {
"account": {
"SideLoadingDisabled": "Your Microsoft 365 tenant admin hasn't enabled sideloading permission for your account. You can't install your app to Teams!",
"NotSignIn": "You have not logged in to your Microsoft 365 account yet. Please use teamsapp auth login command to login to your Microsoft 365 account.",
"SignInSuccess": "Microsoft 365 Account (%s) is logged in and sideloading permission is enabled."
},
"node" : {
"NotFound": "Cannot find Node.js. Node.js used for developing Teams apps with JavaScript or TypeScript using Teams Toolkit for Visual Studio Code. Visit https://nodejs.org/about/releases/ to install the LTS version.",
"NotSupported": "Node.js (%s) is not the officially supported version (%s). Your project may continue to work but we recommend to install the supported version.",
"Success": "Node.js version (%s) is installed."
},
"dotnet" : {
"NotFound": "Cannot find .NET Core SDK. .NET Core SDK is used for developing Teams app in C# using Teams Toolkit for Visual Studio. Visit https://dotnet.microsoft.com/en-us/download to install the LTS version.",
"NotSupported": ".NET Core SDK (%s) is not the officially supported version (%s).",
"Success": ".NET Core SDK version (%s) is installed."
},
"func" : {
"NotFound": "Cannot find Azure Functions Core Tools.",
"NotSupported": "Azure Functions Core Tools (%s) is not the officially supported version (%s).",
"Success": "Azure Functions Core Tools version (%s) is installed."
},
"cert" : {
"NotFound": "Cannot find local Certificate.",
"FoundNotTrust": "Local Certificate is found but not trusted.",
"Success": "Local Certificate is found and is trusted."
}
}
}
}
2 changes: 2 additions & 0 deletions packages/cli/src/telemetry/cliTelemetryEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ export enum TelemetryEvent {
M365Sigeloading = "m365-sideloading",
M365Unacquire = "m365-unacquire",
M365LaunchInfo = "m365-launch-info",

Doctor = "doctor",
}

export enum TelemetryProperty {
Expand Down
Loading

0 comments on commit bb3771d

Please sign in to comment.