diff --git a/.eslintrc.cjs b/.eslintrc.cjs index b12918a..eee3cd7 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,9 +1,3 @@ -/* - * Copyright (c) 2020, salesforce.com, inc. - * All rights reserved. - * Licensed under the BSD 3-Clause license. - * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ module.exports = { extends: ['eslint-config-salesforce-typescript', 'eslint-config-salesforce-license', 'plugin:sf-plugin/recommended'], root: true, diff --git a/.nycrc b/.nycrc index fc0836f..d363b5a 100644 --- a/.nycrc +++ b/.nycrc @@ -1,3 +1,5 @@ { - "extends": "@salesforce/dev-config/nyc" + "nyc": { + "extends": "@salesforce/dev-config/nyc" + } } diff --git a/README.md b/README.md index d1407df..64c1c0d 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,13 @@ -**NOTE: This template for sf plugins is not yet official. Please consult with the Platform CLI team before using this template.** - -# plugin-license-management +# License Management Plugin [![NPM](https://img.shields.io/npm/v/@salesforce/plugin-license-management.svg?label=@salesforce/plugin-license-management)](https://www.npmjs.com/package/@salesforce/plugin-license-management) [![Downloads/week](https://img.shields.io/npm/dw/@salesforce/plugin-license-management.svg)](https://npmjs.org/package/@salesforce/plugin-license-management) [![License](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](https://opensource.org/license/apache-2-0) -## Using the template - -This repository provides a template for creating a plugin for the Salesforce CLI. To convert this template to a working plugin: - -1. Please get in touch with the Platform CLI team. We want to help you develop your plugin. -2. Generate your plugin: - - ``` - sf plugins install dev - sf dev generate plugin - - git init -b main - git add . && git commit -m "chore: initial commit" - ``` - -3. Create your plugin's repo in the salesforcecli github org -4. When you're ready, replace the contents of this README with the information you want. - -## Learn about `sf` plugins - -Salesforce CLI plugins are based on the [oclif plugin framework](https://oclif.io/docs/introduction). Read the [plugin developer guide](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_plugins.meta/sfdx_cli_plugins/cli_plugins_architecture_sf_cli.htm) to learn about Salesforce CLI plugin development. - -This repository contains a lot of additional scripts and tools to help with general Salesforce node development and enforce coding standards. You should familiarize yourself with some of the [node developer packages](#tooling) used by Salesforce. There is also a default circleci config using the [release management orb](https://github.com/forcedotcom/npm-release-management-orb) standards. +Manage Permission Set Licenses (PSLs) in your scratch orgs. This plugin lets you provision PSL seats into a target scratch org — either one at a time via command line flags, or in bulk using a JSON definition file. -Additionally, there are some additional tests that the Salesforce CLI will enforce if this plugin is ever bundled with the CLI. These test are included by default under the `posttest` script and it is required to keep these tests active in your plugin if you plan to have it bundled. +## Before You Begin -### Tooling - -- [@salesforce/core](https://github.com/forcedotcom/sfdx-core) -- [@salesforce/kit](https://github.com/forcedotcom/kit) -- [@salesforce/sf-plugins-core](https://github.com/salesforcecli/sf-plugins-core) -- [@salesforce/ts-types](https://github.com/forcedotcom/ts-types) -- [@salesforce/ts-sinon](https://github.com/forcedotcom/ts-sinon) -- [@salesforce/dev-config](https://github.com/forcedotcom/dev-config) -- [@salesforce/dev-scripts](https://github.com/forcedotcom/dev-scripts) - -# Everything past here is only a suggestion as to what should be in your specific plugin's description - -This plugin is bundled with the [Salesforce CLI](https://developer.salesforce.com/tools/sfdxcli). For more information on the CLI, read the [getting started guide](https://developer.salesforce.com/docs/atlas.en-us.sfdx_setup.meta/sfdx_setup/sfdx_setup_intro.htm). - -We always recommend using the latest version of these commands bundled with the CLI, however, you can install a specific version or tag if needed. +- Install and authenticate the [Salesforce CLI](https://developer.salesforce.com/tools/sfdxcli). +- Ensure you have the appropriate permissions to manage Permission Set Licenses in the target org. ## Install @@ -170,12 +133,53 @@ _See code: [src/commands/license/provision.ts](https://github.com/salesforcecli/ # Local Testing +### 1. Log in to your Dev Hub + +```bash +sf org login web --set-default-dev-hub --instance-url +``` + +### 2. Create a scratch org + +Before creating the scratch org, update `test/config/scratch-org-def.json` with your values: + +| Field | Description | +| ----------- | ------------------------------------ | +| `orgName` | Display name for the scratch org | +| `namespace` | Your package namespace (e.g. `myNS`) | + +```bash +sf org create scratch --definition-file test/config/scratch-org-def.json --alias +``` + +### 3. Install the package into the scratch org + +Make the package available in the scratch org. Some ways to do this include: + +**Install a released package version** + +```bash +sf package install --package --target-org +``` + +**Push source directly** + ```bash -sf org create scratch --target-dev-hub --definition-file test/config/scratch-org-def.json +sf project deploy start --target-org +``` -sf package install --package --target-org +### 4. Open the scratch org (optional) -sf package install report -i -o +```bash +sf org open --target-org +``` + +### 5. Provision licenses + +```bash +# Provision a single PSL +sf license provision --target-org --namespace --license --quantity -sf license provision -o --license premium --namespace demo --quantity 10 +# Provision multiple PSLs using a definition file +sf license provision --target-org --definition-file ``` diff --git a/messages/license.provision.md b/messages/license.provision.md index 84386a6..97c4e15 100644 --- a/messages/license.provision.md +++ b/messages/license.provision.md @@ -8,7 +8,7 @@ Provision Permission Set Licenses (PSL) into the target org. Successful executio There are two ways to run this command. You can provide the information to identify a single PSL via command line flags, or provision multiple PSLs in a single call by supplying a JSON formatted file. -See for the format and options contained within the JSON file. +See https://github.com/salesforcecli/plugin-license-management#sf-license-provision for the format and options contained within the JSON file. # flags.namespace.summary @@ -50,7 +50,11 @@ The definition file must contain at least one license entry. # error.unsupportedDefinitionFileFields -Nonexistent fields: %s +Unknown fields in definition file: %s + +# error.invalidDefinitionFileJson + +Definition file contains invalid JSON: %s # error.missingRequiredDefinitionFileFields diff --git a/package.json b/package.json index 804663f..03b08fb 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,12 @@ { "name": "@salesforce/plugin-license-management", - "description": "Manage Permission Set Licenses ", + "description": "Manage Permission Set Licenses", "version": "1.0.0", "author": "Salesforce", "bugs": "https://github.com/forcedotcom/cli/issues", "dependencies": { "@oclif/core": "^4", "@salesforce/core": "^8.28.1", - "@salesforce/kit": "^3.2.6", "@salesforce/sf-plugins-core": "^12" }, "devDependencies": { diff --git a/src/commands/license/provision.ts b/src/commands/license/provision.ts index eefcc90..5b7cb00 100644 --- a/src/commands/license/provision.ts +++ b/src/commands/license/provision.ts @@ -28,7 +28,7 @@ type ProvisionLicenseSpec = { }; type ApiLicenseSpec = { - namespacePrefix?: string; + namespace?: string; permissionSetLicense?: string; quantity?: number; }; @@ -60,7 +60,7 @@ function getLicenseDefinitionName(spec: ProvisionLicenseSpec): string { function toApiSpec(spec: ProvisionLicenseSpec): ApiLicenseSpec { return { - namespacePrefix: spec.namespace, + namespace: spec.namespace, permissionSetLicense: spec.license, quantity: spec.quantity, }; @@ -77,8 +77,8 @@ export default class LicenseProvision extends SfCommand namespace: Flags.string({ char: 'n', summary: messages.getMessage('flags.namespace.summary'), - dependsOn: ['license'], exclusive: ['definition-file'], + relationships: [{ type: 'all', flags: ['license', 'quantity'] }], }), license: Flags.string({ char: 'l', @@ -91,20 +91,26 @@ export default class LicenseProvision extends SfCommand summary: messages.getMessage('flags.quantity.summary'), min: 0, max: Number.MAX_SAFE_INTEGER, - dependsOn: ['license'], exclusive: ['definition-file'], + relationships: [{ type: 'all', flags: ['namespace', 'license'] }], }), - 'definition-file': Flags.string({ + 'definition-file': Flags.file({ char: 'f', summary: messages.getMessage('flags.definition-file.summary'), exclusive: ['license', 'namespace', 'quantity'], + exists: true, }), }; // Protected to allow stubbing in tests protected static async loadSpecsFromFile(filePath: string): Promise { const fileContent = await readFile(filePath, 'utf-8'); - const definition = JSON.parse(fileContent) as DefinitionFile; + let definition: DefinitionFile; + try { + definition = JSON.parse(fileContent) as DefinitionFile; + } catch (e) { + throw messages.createError('error.invalidDefinitionFileJson', [(e as Error).message]); + } if (!Array.isArray(definition.licenses) || definition.licenses.length === 0) { throw messages.createError('error.emptyDefinitionFile'); diff --git a/test/.eslintrc.cjs b/test/.eslintrc.cjs index fadf2c9..831045c 100644 --- a/test/.eslintrc.cjs +++ b/test/.eslintrc.cjs @@ -1,10 +1,3 @@ -/* - * Copyright (c) 2020, salesforce.com, inc. - * All rights reserved. - * Licensed under the BSD 3-Clause license. - * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ - module.exports = { extends: '../.eslintrc.cjs', // Allow describe and it diff --git a/test/commands/license/provision.test.ts b/test/commands/license/provision.test.ts index 83c961b..5916c3b 100644 --- a/test/commands/license/provision.test.ts +++ b/test/commands/license/provision.test.ts @@ -107,7 +107,7 @@ describe('license provision', () => { const body = JSON.parse(callArgs.body) as { licenses: unknown[] }; expect(body.licenses).to.have.length(1); expect(body.licenses[0]).to.deep.include({ - namespacePrefix: 'demo', + namespace: 'demo', permissionSetLicense: 'newLicense', quantity: 5, }); @@ -285,7 +285,20 @@ describe('license provision', () => { await LicenseProvision.run(['--target-org', testOrg.username, '--definition-file', tmpFilePath]); expect.fail('Expected an error to be thrown'); } catch (error: unknown) { - expect((error as Error).message).to.include('Nonexistent fields: startDate, endDate'); + expect((error as Error).message).to.include('Unknown fields in definition file: startDate, endDate'); + } finally { + await unlink(tmpFilePath).catch(() => {}); + } + }); + + it('throws a user-friendly error when definition file contains invalid JSON', async () => { + const tmpFilePath = join(tmpdir(), `provision-malformed-${Date.now()}.json`); + await writeFile(tmpFilePath, '{ this is not valid json }'); + try { + await LicenseProvision.run(['--target-org', testOrg.username, '--definition-file', tmpFilePath]); + expect.fail('Expected an error to be thrown'); + } catch (error: unknown) { + expect((error as Error).message).to.include('invalid JSON'); } finally { await unlink(tmpFilePath).catch(() => {}); }