diff --git a/docs/manual/docs/cmd/spo/app/app-get.md b/docs/manual/docs/cmd/spo/app/app-get.md index 3024f3dbe90..14103d5ee78 100644 --- a/docs/manual/docs/cmd/spo/app/app-get.md +++ b/docs/manual/docs/cmd/spo/app/app-get.md @@ -13,7 +13,9 @@ spo app get [options] Option|Description ------|----------- `--help`|output usage information -`-i, --id `|ID of the app to retrieve information for +`-i, --id [id]`|ID of the app to retrieve information for. Specify the `id` or the `name` but not both +`-n, --name [name]`|Name of the app to retrieve information for. Specify the `id` or the `name` but not both +`-u, --appCatalogUrl [appCatalogUrl]`|URL of the tenant app catalog site. If not specified, the CLI will try to resolve it automatically `-o, --output [output]`|Output type. `json|text`. Default `text` `--verbose`|Runs command with verbose logging `--debug`|Runs command with debug logging @@ -33,6 +35,18 @@ Return details about the app with ID _b2307a39-e878-458b-bc90-03bc578531d6_ avai spo app get -i b2307a39-e878-458b-bc90-03bc578531d6 ``` +Return details about the app with name _solution.sppkg_ available in the tenant app catalog. Will try to detect the app catalog URL + +```sh +spo app get --name solution.sppkg +``` + +Return details about the app with name _solution.sppkg_ available in the tenant app catalog using the specified app catalog URL + +```sh +spo app get --name solution.sppkg --appCatalogUrl https://contoso.sharepoint.com/sites/apps +``` + ## More information - Application Lifecycle Management (ALM) APIs: [https://docs.microsoft.com/en-us/sharepoint/dev/apis/alm-api-for-spfx-add-ins](https://docs.microsoft.com/en-us/sharepoint/dev/apis/alm-api-for-spfx-add-ins) \ No newline at end of file diff --git a/src/o365/spo/commands/app/app-get.spec.ts b/src/o365/spo/commands/app/app-get.spec.ts index 8738cb74ebc..4ecb48d3f94 100644 --- a/src/o365/spo/commands/app/app-get.spec.ts +++ b/src/o365/spo/commands/app/app-get.spec.ts @@ -15,10 +15,12 @@ describe(commands.APP_GET, () => { let cmdInstanceLogSpy: sinon.SinonSpy; let trackEvent: any; let telemetry: any; + let promptOptions: any; before(() => { sinon.stub(auth, 'restoreAuth').callsFake(() => Promise.resolve()); sinon.stub(auth, 'ensureAccessToken').callsFake(() => { return Promise.resolve('ABC'); }); + sinon.stub(auth, 'getAccessToken').callsFake(() => { return Promise.resolve('ABC'); }); trackEvent = sinon.stub(appInsights, 'trackEvent').callsFake((t) => { telemetry = t; }); @@ -30,6 +32,10 @@ describe(commands.APP_GET, () => { cmdInstance = { log: (msg: string) => { log.push(msg); + }, + prompt: (options: any, cb: (result: { continue: boolean }) => void) => { + promptOptions = options; + cb({ continue: false }); } }; cmdInstanceLogSpy = sinon.spy(cmdInstance, 'log'); @@ -38,15 +44,18 @@ describe(commands.APP_GET, () => { }); afterEach(() => { - Utils.restore(vorpal.find); + Utils.restore([ + vorpal.find, + request.get + ]); }); after(() => { Utils.restore([ appInsights.trackEvent, auth.ensureAccessToken, - auth.restoreAuth, - request.get + auth.getAccessToken, + auth.restoreAuth ]); }); @@ -99,19 +108,19 @@ describe(commands.APP_GET, () => { }); }); - it('retrieves information about available app from the tenant app catalog (debug)', (done) => { + it('retrieves information about the app with the specified id from the tenant app catalog (debug)', (done) => { sinon.stub(request, 'get').callsFake((opts) => { if (opts.url.indexOf(`/_api/web/tenantappcatalog/AvailableApps/GetById('b2307a39-e878-458b-bc90-03bc578531d6')`) > -1) { if (opts.headers.authorization && opts.headers.authorization.indexOf('Bearer ') === 0 && opts.headers.accept && opts.headers.accept.indexOf('application/json') === 0) { - return Promise.resolve(JSON.stringify({ + return Promise.resolve({ ID: 'b2307a39-e878-458b-bc90-03bc578531d6', Title: 'online-client-side-solution', Deployed: true, AppCatalogVersion: '1.0.0.0' - })); + }); } } @@ -142,19 +151,19 @@ describe(commands.APP_GET, () => { }); }); - it('retrieves information about available app from the tenant app catalog', (done) => { + it('retrieves information about the app with the specified id from the tenant app catalog', (done) => { sinon.stub(request, 'get').callsFake((opts) => { if (opts.url.indexOf(`/_api/web/tenantappcatalog/AvailableApps/GetById('b2307a39-e878-458b-bc90-03bc578531d6')`) > -1) { if (opts.headers.authorization && opts.headers.authorization.indexOf('Bearer ') === 0 && opts.headers.accept && opts.headers.accept.indexOf('application/json') === 0) { - return Promise.resolve(JSON.stringify({ + return Promise.resolve({ ID: 'b2307a39-e878-458b-bc90-03bc578531d6', Title: 'online-client-side-solution', Deployed: true, AppCatalogVersion: '1.0.0.0' - })); + }); } } @@ -185,6 +194,282 @@ describe(commands.APP_GET, () => { }); }); + it('retrieves information about the app with the specified name from the specified tenant app catalog', (done) => { + sinon.stub(request, 'get').callsFake((opts) => { + if (opts.url.indexOf(`/_api/web/tenantappcatalog/AvailableApps/GetById('b2307a39-e878-458b-bc90-03bc578531d6')`) > -1) { + if (opts.headers.authorization && + opts.headers.authorization.indexOf('Bearer ') === 0 && + opts.headers.accept && + opts.headers.accept.indexOf('application/json') === 0) { + return Promise.resolve({ + ID: 'b2307a39-e878-458b-bc90-03bc578531d6', + Title: 'online-client-side-solution', + Deployed: true, + AppCatalogVersion: '1.0.0.0' + }); + } + } + + if (opts.url === `https://contoso.sharepoint.com/sites/apps/_api/web/getfolderbyserverrelativeurl('AppCatalog')/files('solution.sppkg')?$select=UniqueId`) { + return Promise.resolve({ + UniqueId: 'b2307a39-e878-458b-bc90-03bc578531d6' + }); + } + + return Promise.reject('Invalid request'); + }); + + auth.site = new Site(); + auth.site.connected = true; + auth.site.url = 'https://contoso-admin.sharepoint.com'; + auth.site.tenantId = 'abc'; + cmdInstance.action = command.action(); + cmdInstance.action({ options: { debug: false, name: 'solution.sppkg', appCatalogUrl: 'https://contoso.sharepoint.com/sites/apps' } }, () => { + try { + assert(cmdInstanceLogSpy.calledWith({ + ID: 'b2307a39-e878-458b-bc90-03bc578531d6', + Title: 'online-client-side-solution', + Deployed: true, + AppCatalogVersion: '1.0.0.0' + })); + done(); + } + catch (e) { + done(e); + } + finally { + Utils.restore(request.get); + } + }); + }); + + it('retrieves information about the app with the specified name with auto-discovered tenant app catalog (debug)', (done) => { + sinon.stub(request, 'get').callsFake((opts) => { + if (opts.url.indexOf(`/_api/search/query?querytext='contentclass:STS_Site%20AND%20SiteTemplate:APPCATALOG'&SelectProperties='SPWebUrl'`) > -1) { + return Promise.resolve({ + PrimaryQueryResult: { + RelevantResults: { + Table: { + Rows: [{ + Cells: [{ + Key: 'SPWebUrl', + Value: 'https://contoso.sharepoint.com/sites/apps' + }] + }] + } + } + } + }); + } + + if (opts.url.indexOf(`/_api/web/tenantappcatalog/AvailableApps/GetById('b2307a39-e878-458b-bc90-03bc578531d6')`) > -1) { + if (opts.headers.authorization && + opts.headers.authorization.indexOf('Bearer ') === 0 && + opts.headers.accept && + opts.headers.accept.indexOf('application/json') === 0) { + return Promise.resolve({ + ID: 'b2307a39-e878-458b-bc90-03bc578531d6', + Title: 'online-client-side-solution', + Deployed: true, + AppCatalogVersion: '1.0.0.0' + }); + } + } + + if (opts.url === `https://contoso.sharepoint.com/sites/apps/_api/web/getfolderbyserverrelativeurl('AppCatalog')/files('solution.sppkg')?$select=UniqueId`) { + return Promise.resolve({ + UniqueId: 'b2307a39-e878-458b-bc90-03bc578531d6' + }); + } + + return Promise.reject('Invalid request'); + }); + + auth.site = new Site(); + auth.site.connected = true; + auth.site.url = 'https://contoso-admin.sharepoint.com'; + auth.site.tenantId = 'abc'; + cmdInstance.action = command.action(); + cmdInstance.action({ options: { debug: true, name: 'solution.sppkg' } }, () => { + try { + assert(cmdInstanceLogSpy.calledWith({ + ID: 'b2307a39-e878-458b-bc90-03bc578531d6', + Title: 'online-client-side-solution', + Deployed: true, + AppCatalogVersion: '1.0.0.0' + })); + done(); + } + catch (e) { + done(e); + } + finally { + Utils.restore(request.get); + } + }); + }); + + it('prompts for tenant app catalog URL when no app catalog site found (debug)', (done) => { + sinon.stub(request, 'get').callsFake((opts) => { + if (opts.url.indexOf(`search/query?querytext='contentclass:STS_Site%20AND%20SiteTemplate:APPCATALOG'`) > -1) { + return Promise.resolve({ + PrimaryQueryResult: { + RelevantResults: { + RowCount: 0, + Table: {} + } + } + }); + } + + return Promise.reject('Invalid request'); + }); + + auth.site = new Site(); + auth.site.connected = true; + auth.site.url = 'https://contoso.sharepoint.com'; + cmdInstance.action = command.action(); + cmdInstance.action({ options: { debug: true, name: 'solution.sppkg' } }, () => { + let promptIssued = false; + + if (promptOptions && promptOptions.name === 'appCatalogUrl') { + promptIssued = true; + } + + try { + assert(promptIssued); + done(); + } + catch (e) { + done(e); + } + }); + }); + + it('fails when no URL provided in the prompt for tenant app catalog URL', (done) => { + let solutionLookedUp: boolean = false; + + sinon.stub(request, 'get').callsFake((opts) => { + if (opts.url.indexOf(`search/query?querytext='contentclass:STS_Site%20AND%20SiteTemplate:APPCATALOG'`) > -1) { + return Promise.reject('Error while executing search query'); + } + + if (opts.url === `https://contoso.sharepoint.com/sites/apps/_api/web/getfolderbyserverrelativeurl('AppCatalog')/files('solution.sppkg')?$select=UniqueId`) { + solutionLookedUp = true; + return Promise.resolve({ + UniqueId: 'b2307a39-e878-458b-bc90-03bc578531d6' + }); + } + + return Promise.reject('Invalid request'); + }); + + auth.site = new Site(); + auth.site.connected = true; + auth.site.url = 'https://contoso.sharepoint.com'; + cmdInstance.action = command.action(); + cmdInstance.prompt = (options: any, cb: (result: { appCatalogUrl: string }) => void) => { + cb({ appCatalogUrl: '' }); + }; + cmdInstance.action({ options: { debug: false, name: 'solution.sppkg' } }, () => { + try { + assert.equal(solutionLookedUp, false); + done(); + } + catch (e) { + done(e); + } + }); + }); + + it('fails when the URL provided in the prompt for tenant app catalog URL is not a valid SharePoint URL', (done) => { + let solutionLookedUp: boolean = false; + + sinon.stub(request, 'get').callsFake((opts) => { + if (opts.url.indexOf(`search/query?querytext='contentclass:STS_Site%20AND%20SiteTemplate:APPCATALOG'`) > -1) { + return Promise.reject('Error while executing search query'); + } + + if (opts.url === `https://contoso.sharepoint.com/sites/apps/_api/web/getfolderbyserverrelativeurl('AppCatalog')/files('solution.sppkg')?$select=UniqueId`) { + solutionLookedUp = true; + return Promise.resolve({ + UniqueId: 'b2307a39-e878-458b-bc90-03bc578531d6' + }); + } + + return Promise.reject('Invalid request'); + }); + + auth.site = new Site(); + auth.site.connected = true; + auth.site.url = 'https://contoso.sharepoint.com'; + cmdInstance.action = command.action(); + cmdInstance.prompt = (options: any, cb: (result: { appCatalogUrl: string }) => void) => { + cb({ appCatalogUrl: 'url' }); + }; + cmdInstance.action({ options: { debug: false, name: 'solution.sppkg' } }, () => { + try { + assert.equal(solutionLookedUp, false); + done(); + } + catch (e) { + done(e); + } + }); + }); + + it('retrieves information about the app with the specified name from the specified tenant app catalog', (done) => { + sinon.stub(request, 'get').callsFake((opts) => { + if (opts.url.indexOf(`search/query?querytext='contentclass:STS_Site%20AND%20SiteTemplate:APPCATALOG'`) > -1) { + return Promise.reject('Error while executing search query'); + } + + if (opts.url === `https://contoso.sharepoint.com/sites/apps/_api/web/getfolderbyserverrelativeurl('AppCatalog')/files('solution.sppkg')?$select=UniqueId`) { + return Promise.resolve({ + UniqueId: 'b2307a39-e878-458b-bc90-03bc578531d6' + }); + } + + if (opts.url.indexOf(`/_api/web/tenantappcatalog/AvailableApps/GetById('b2307a39-e878-458b-bc90-03bc578531d6')`) > -1) { + if (opts.headers.authorization && + opts.headers.authorization.indexOf('Bearer ') === 0 && + opts.headers.accept && + opts.headers.accept.indexOf('application/json') === 0) { + return Promise.resolve({ + ID: 'b2307a39-e878-458b-bc90-03bc578531d6', + Title: 'online-client-side-solution', + Deployed: true, + AppCatalogVersion: '1.0.0.0' + }); + } + } + + return Promise.reject('Invalid request'); + }); + + auth.site = new Site(); + auth.site.connected = true; + auth.site.url = 'https://contoso.sharepoint.com'; + cmdInstance.action = command.action(); + cmdInstance.prompt = (options: any, cb: (result: { appCatalogUrl: string }) => void) => { + cb({ appCatalogUrl: 'https://contoso.sharepoint.com/sites/apps' }); + }; + cmdInstance.action({ options: { debug: false, name: 'solution.sppkg' } }, () => { + try { + assert(cmdInstanceLogSpy.calledWith({ + ID: 'b2307a39-e878-458b-bc90-03bc578531d6', + Title: 'online-client-side-solution', + Deployed: true, + AppCatalogVersion: '1.0.0.0' + })); + done(); + } + catch (e) { + done(e); + } + }); + }); + it('correctly handles no app found in the tenant app catalog', (done) => { sinon.stub(request, 'get').callsFake((opts) => { if (opts.url.indexOf(`/_api/web/tenantappcatalog/AvailableApps/GetById('b2307a39-e878-458b-bc90-03bc578531d6')`) > -1) { @@ -192,7 +477,7 @@ describe(commands.APP_GET, () => { opts.headers.authorization.indexOf('Bearer ') === 0 && opts.headers.accept && opts.headers.accept.indexOf('application/json') === 0) { - return Promise.reject({ error: JSON.stringify({ + return Promise.reject({ error: { 'odata.error': { code: '-1, Microsoft.SharePoint.Client.ResourceNotFoundException', message: { @@ -200,7 +485,7 @@ describe(commands.APP_GET, () => { value: "Exception of type 'Microsoft.SharePoint.Client.ResourceNotFoundException' was thrown." } } - })}); + }}); } } @@ -266,14 +551,14 @@ describe(commands.APP_GET, () => { opts.headers.authorization.indexOf('Bearer ') === 0 && opts.headers.accept && opts.headers.accept.indexOf('application/json') === 0) { - return Promise.reject({ error: JSON.stringify({ + return Promise.reject({ error: { 'odata.error': { code: '-1, Microsoft.SharePoint.Client.InvalidOperationException', message: { value: 'An error has occurred' } } - }) }); + }}); } } @@ -304,20 +589,45 @@ describe(commands.APP_GET, () => { assert.notEqual(actual, true); }); - it('passes validation when the id option specified', () => { + it('fails validation if the specified id is not a valid GUID', () => { const actual = (command.validate() as CommandValidate)({ options: { id: '123' } }); + assert.notEqual(actual, true); + }); + + it('fails validation when both the id and the name options specified', () => { + const actual = (command.validate() as CommandValidate)({ options: { id: 'f8b52a45-61d5-4264-81c9-c3bbd203e7d0', name: 'solution.sppkg' } }); + assert.notEqual(actual, true); + }); + + it('fails validation when the specified appCatalogUrl is not a valid SharePoint URL', () => { + const actual = (command.validate() as CommandValidate)({ options: { name: 'solution.sppkg', appCatalogUrl: 'url' } }); + assert.notEqual(actual, true); + }); + + it('passes validation when the id option specified', () => { + const actual = (command.validate() as CommandValidate)({ options: { id: 'f8b52a45-61d5-4264-81c9-c3bbd203e7d0' } }); + assert.equal(actual, true); + }); + + it('passes validation when the name option specified', () => { + const actual = (command.validate() as CommandValidate)({ options: { name: 'solution.sppkg' } }); + assert.equal(actual, true); + }); + + it('passes validation when the name and appCatalogUrl options specified', () => { + const actual = (command.validate() as CommandValidate)({ options: { name: 'solution.sppkg', appCatalogUrl: 'https://contoso.sharepoint.com/sites/apps' } }); assert.equal(actual, true); }); it('supports debug mode', () => { const options = (command.options() as CommandOption[]); - let containsdebugOption = false; + let containsDebugOption = false; options.forEach(o => { if (o.option === '--debug') { - containsdebugOption = true; + containsDebugOption = true; } }); - assert(containsdebugOption); + assert(containsDebugOption); }); it('has help referring to the right command', () => { diff --git a/src/o365/spo/commands/app/app-get.ts b/src/o365/spo/commands/app/app-get.ts index 2479e9eef73..423fd3df946 100644 --- a/src/o365/spo/commands/app/app-get.ts +++ b/src/o365/spo/commands/app/app-get.ts @@ -1,4 +1,5 @@ import auth from '../../SpoAuth'; +import { Auth } from '../../../../Auth'; import config from '../../../../config'; import commands from '../../commands'; import GlobalOptions from '../../../../GlobalOptions'; @@ -18,7 +19,9 @@ interface CommandArgs { } interface Options extends GlobalOptions { - id: string; + id?: string; + name?: string; + appCatalogUrl?: string; } class AppGetCommand extends SpoCommand { @@ -30,6 +33,14 @@ class AppGetCommand extends SpoCommand { return 'Gets information about the specific app from the tenant app catalog'; } + public getTelemetryProperties(args: CommandArgs): any { + const telemetryProps: any = super.getTelemetryProperties(args); + telemetryProps.id = typeof args.options.id !== 'undefined'; + telemetryProps.name = typeof args.options.name !== 'undefined'; + telemetryProps.appCatalogUrl = typeof args.options.appCatalogUrl !== 'undefined'; + return telemetryProps; + } + protected requiresTenantAdmin(): boolean { return false; } @@ -37,21 +48,111 @@ class AppGetCommand extends SpoCommand { public commandAction(cmd: CommandInstance, args: CommandArgs, cb: () => void): void { auth .ensureAccessToken(auth.service.resource, cmd, this.debug) - .then((accessToken: string): request.RequestPromise => { + .then((accessToken: string): request.RequestPromise | Promise => { if (this.debug) { - cmd.log(`Retrieved access token ${accessToken}. Loading apps from tenant app catalog...`); + cmd.log(`Retrieved access token ${accessToken}...`); + } + + if (args.options.id) { + return Promise.resolve(args.options.id); } + else { + let appCatalogUrl: string = ''; + + return (args.options.appCatalogUrl ? + Promise.resolve(args.options.appCatalogUrl) : + this.getTenantAppCatalogUrl(cmd, this.debug)) + .then((appCatalogUrl: string): Promise => { + return Promise.resolve(appCatalogUrl); + }, (error: any): Promise => { + if (this.debug) { + cmd.log('Error'); + cmd.log(error); + cmd.log(''); + } + + return new Promise((resolve: (appCatalogUrl: string) => void, reject: (error: any) => void): void => { + cmd.log('CLI could not automatically determine the URL of the tenant app catalog'); + cmd.log('What is the absolute URL of your tenant app catalog site'); + cmd.prompt({ + type: 'input', + name: 'appCatalogUrl', + message: '? ', + }, (result: { appCatalogUrl?: string }): void => { + if (!result.appCatalogUrl) { + reject(`Couldn't determine tenant app catalog URL`); + } + else { + let isValidSharePointUrl: boolean | string = SpoCommand.isValidSharePointUrl(result.appCatalogUrl); + if (isValidSharePointUrl === true) { + resolve(result.appCatalogUrl); + } + else { + reject(isValidSharePointUrl); + } + } + }); + }); + }) + .then((appCatalog: string): Promise => { + if (this.debug) { + cmd.log(`Retrieved tenant app catalog URL ${appCatalog}`); + } + + appCatalogUrl = appCatalog; + + if (this.verbose) { + cmd.log(`Retrieving access token for the app catalog at ${appCatalogUrl}...`); + } + + const appCatalogResource: string = Auth.getResourceFromUrl(appCatalogUrl); + return auth.getAccessToken(appCatalogResource, auth.service.refreshToken as string, cmd, this.debug); + }) + .then((token: string): request.RequestPromise => { + if (this.verbose) { + cmd.log(`Looking up app id for app named ${args.options.name}...`); + } + + const requestOptions: any = { + url: `${appCatalogUrl}/_api/web/getfolderbyserverrelativeurl('AppCatalog')/files('${args.options.name}')?$select=UniqueId`, + headers: Utils.getRequestHeaders({ + authorization: `Bearer ${token}`, + accept: 'application/json;odata=nometadata' + }), + json: true + }; + + if (this.debug) { + cmd.log('Executing web request...'); + cmd.log(requestOptions); + cmd.log(''); + } + + return request.get(requestOptions); + }) + .then((res: { UniqueId: string }): Promise => { + if (this.debug) { + cmd.log('Response:'); + cmd.log(res); + cmd.log(''); + } + return Promise.resolve(res.UniqueId); + }); + } + }) + .then((appId: string): request.RequestPromise => { if (this.verbose) { - cmd.log(`Retrieving app information...`); + cmd.log(`Retrieving information for app ${appId}...`); } const requestOptions: any = { - url: `${auth.site.url}/_api/web/tenantappcatalog/AvailableApps/GetById('${encodeURIComponent(args.options.id)}')`, + url: `${auth.site.url}/_api/web/tenantappcatalog/AvailableApps/GetById('${encodeURIComponent(appId)}')`, headers: Utils.getRequestHeaders({ - authorization: `Bearer ${accessToken}`, + authorization: `Bearer ${auth.service.accessToken}`, accept: 'application/json;odata=nometadata' - }) + }), + json: true }; if (this.debug) { @@ -62,25 +163,34 @@ class AppGetCommand extends SpoCommand { return request.get(requestOptions); }) - .then((res: string): void => { + .then((res: AppMetadata): void => { if (this.debug) { cmd.log('Response:'); cmd.log(res); cmd.log(''); } - const app: AppMetadata = JSON.parse(res); - cmd.log(app); + cmd.log(res); cb(); - }, (rawRes: any): void => this.handleRejectedODataPromise(rawRes, cmd, cb)); + }, (rawRes: any): void => this.handleRejectedODataJsonPromise(rawRes, cmd, cb)); } public options(): CommandOption[] { - const options: CommandOption[] = [{ - option: '-i, --id ', - description: 'ID of the app to retrieve information for' - }]; + const options: CommandOption[] = [ + { + option: '-i, --id [id]', + description: 'ID of the app to retrieve information for. Specify the id or the name but not both' + }, + { + option: '-n, --name [name]', + description: 'Name of the app to retrieve information for. Specify the id or the name but not both' + }, + { + option: '-u, --appCatalogUrl [appCatalogUrl]', + description: 'URL of the tenant app catalog site. If not specified, the CLI will try to resolve it automatically' + } + ]; const parentOptions: CommandOption[] = super.options(); return options.concat(parentOptions); @@ -88,8 +198,20 @@ class AppGetCommand extends SpoCommand { public validate(): CommandValidate { return (args: CommandArgs): boolean | string => { - if (!args.options.id) { - return 'Required parameter id missing'; + if (!args.options.id && !args.options.name) { + return 'Specify either the id or the name'; + } + + if (args.options.id && args.options.name) { + return 'Specify either the id or the name but not both'; + } + + if (args.options.id && !Utils.isValidGuid(args.options.id)) { + return `${args.options.id} is not a valid GUID`; + } + + if (args.options.appCatalogUrl) { + return SpoCommand.isValidSharePointUrl(args.options.appCatalogUrl); } return true; @@ -113,7 +235,15 @@ class AppGetCommand extends SpoCommand { Return details about the app with ID ${chalk.grey('b2307a39-e878-458b-bc90-03bc578531d6')} available in the tenant app catalog. - ${chalk.grey(config.delimiter)} ${commands.APP_GET} -i b2307a39-e878-458b-bc90-03bc578531d6 + ${chalk.grey(config.delimiter)} ${commands.APP_GET} --id b2307a39-e878-458b-bc90-03bc578531d6 + + Return details about the app with name ${chalk.grey('solution.sppkg')} + available in the tenant app catalog. Will try to detect the app catalog URL + ${chalk.grey(config.delimiter)} ${commands.APP_GET} --name solution.sppkg + + Return details about the app with name ${chalk.grey('solution.sppkg')} + available in the tenant app catalog using the specified app catalog URL + ${chalk.grey(config.delimiter)} ${commands.APP_GET} --name solution.sppkg --appCatalogUrl https://contoso.sharepoint.com/sites/apps More information: