diff --git a/oclif.manifest.json b/oclif.manifest.json index cf17bc076..7b1ce8a71 100644 --- a/oclif.manifest.json +++ b/oclif.manifest.json @@ -1,5 +1,5 @@ { - "version": "5.21.2", + "version": "6.0.0", "commands": { "authCommand": { "id": "authCommand", @@ -1194,7 +1194,7 @@ "environments:get": { "id": "environments:get", "description": "Retrieve Environments from the management API", - "strict": true, + "strict": false, "pluginName": "@devcycle/cli", "pluginAlias": "@devcycle/cli", "pluginType": "core", @@ -1203,6 +1203,9 @@ "hiddenAliases": [], "examples": [ "<%= config.bin %> <%= command.id %>", + "<%= config.bin %> <%= command.id %> environment-one", + "<%= config.bin %> <%= command.id %> environment-one environment-two", + "<%= config.bin %> <%= command.id %> environment-one,environment-two", "<%= config.bin %> <%= command.id %> --keys=environment-one,environment-two" ], "flags": { @@ -1295,7 +1298,13 @@ "multiple": false } }, - "args": {} + "args": { + "keys": { + "name": "keys", + "description": "Environment keys to fetch (space-separated or comma-separated)", + "required": false + } + } }, "environments:list": { "id": "environments:list", @@ -1761,7 +1770,7 @@ "features:get": { "id": "features:get", "description": "Retrieve Features from the Management API", - "strict": true, + "strict": false, "pluginName": "@devcycle/cli", "pluginAlias": "@devcycle/cli", "pluginType": "core", @@ -1770,6 +1779,9 @@ "hiddenAliases": [], "examples": [ "<%= config.bin %> <%= command.id %>", + "<%= config.bin %> <%= command.id %> feature-one", + "<%= config.bin %> <%= command.id %> feature-one feature-two", + "<%= config.bin %> <%= command.id %> feature-one,feature-two", "<%= config.bin %> <%= command.id %> --keys=feature-one,feature-two" ], "flags": { @@ -1880,7 +1892,13 @@ "multiple": false } }, - "args": {} + "args": { + "keys": { + "name": "keys", + "description": "Feature keys to fetch (space-separated or comma-separated)", + "required": false + } + } }, "features:list": { "id": "features:list", @@ -5198,13 +5216,21 @@ }, "variables:get": { "id": "variables:get", - "strict": true, + "description": "Retrieve Variables from the Management API", + "strict": false, "pluginName": "@devcycle/cli", "pluginAlias": "@devcycle/cli", "pluginType": "core", "hidden": false, "aliases": [], "hiddenAliases": [], + "examples": [ + "<%= config.bin %> <%= command.id %>", + "<%= config.bin %> <%= command.id %> var-one", + "<%= config.bin %> <%= command.id %> var-one var-two", + "<%= config.bin %> <%= command.id %> var-one,var-two", + "<%= config.bin %> <%= command.id %> --keys=var-one,var-two" + ], "flags": { "config-path": { "name": "config-path", @@ -5313,7 +5339,13 @@ "multiple": false } }, - "args": {} + "args": { + "keys": { + "name": "keys", + "description": "Variable keys to fetch (space-separated or comma-separated)", + "required": false + } + } }, "variables:list": { "id": "variables:list", diff --git a/src/commands/environments/get.test.ts b/src/commands/environments/get.test.ts new file mode 100644 index 000000000..99cb5f5d8 --- /dev/null +++ b/src/commands/environments/get.test.ts @@ -0,0 +1,71 @@ +import { expect } from '@oclif/test' +import { dvcTest } from '../../../test-utils' +import { BASE_URL } from '../../api/common' + +describe('environments get', () => { + const projectKey = 'test-project' + const authFlags = [ + '--client-id', + 'test-client-id', + '--client-secret', + 'test-client-secret', + ] + + const mockEnvironments = [ + { + key: 'development', + name: 'Development', + _id: '61450f3daec96f5cf4a49960', + }, + { + key: 'production', + name: 'Production', + _id: '61450f3daec96f5cf4a49961', + }, + ] + + dvcTest() + .nock(BASE_URL, (api) => + api + .get(`/v1/projects/${projectKey}/environments`) + .reply(200, mockEnvironments), + ) + .stdout() + .command([ + 'environments get', + '--project', + projectKey, + '--headless', + ...authFlags, + ]) + .it('returns a list of environment objects in headless mode', (ctx) => { + expect(ctx.stdout).to.contain(JSON.stringify(mockEnvironments)) + }) + + // Test positional arguments functionality + dvcTest() + .nock(BASE_URL, (api) => + api + .get(`/v1/projects/${projectKey}/environments/development`) + .reply(200, mockEnvironments[0]) + .get(`/v1/projects/${projectKey}/environments/production`) + .reply(200, mockEnvironments[1]), + ) + .stdout() + .command([ + 'environments get', + 'development', + 'production', + '--project', + projectKey, + ...authFlags, + ]) + .it( + 'fetches multiple environments by space-separated positional arguments', + (ctx) => { + expect(ctx.stdout).to.contain( + JSON.stringify(mockEnvironments, null, 2), + ) + }, + ) +}) diff --git a/src/commands/environments/get.ts b/src/commands/environments/get.ts index 3d90c1de1..a5f9160e4 100644 --- a/src/commands/environments/get.ts +++ b/src/commands/environments/get.ts @@ -1,4 +1,4 @@ -import { Flags } from '@oclif/core' +import { Args, Flags } from '@oclif/core' import inquirer from '../../ui/autocomplete' import { fetchEnvironments, @@ -7,14 +7,26 @@ import { import { EnvironmentPromptResult, environmentPrompt } from '../../ui/prompts' import Base from '../base' import { batchRequests } from '../../utils/batchRequests' +import { parseKeysFromArgs } from '../../utils/parseKeysFromArgs' export default class DetailedEnvironments extends Base { static hidden = false static description = 'Retrieve Environments from the management API' static examples = [ '<%= config.bin %> <%= command.id %>', + '<%= config.bin %> <%= command.id %> environment-one', + '<%= config.bin %> <%= command.id %> environment-one environment-two', + '<%= config.bin %> <%= command.id %> environment-one,environment-two', '<%= config.bin %> <%= command.id %> --keys=environment-one,environment-two', ] + static args = { + keys: Args.string({ + description: + 'Environment keys to fetch (space-separated or comma-separated)', + required: false, + }), + } + static strict = false static flags = { ...Base.flags, keys: Flags.string({ @@ -25,12 +37,13 @@ export default class DetailedEnvironments extends Base { authRequired = true public async run(): Promise { - const { flags } = await this.parse(DetailedEnvironments) - const keys = flags['keys']?.split(',') + const { args, argv, flags } = await this.parse(DetailedEnvironments) const { headless, project } = flags await this.requireProject(project, headless) - if (keys) { + const keys = parseKeysFromArgs(args, argv, flags) + + if (keys && keys.length > 0) { const environments = await batchRequests(keys, (key) => fetchEnvironmentByKey(this.authToken, this.projectKey, key), ) diff --git a/src/commands/features/get.test.ts b/src/commands/features/get.test.ts index 6723c202c..f2ca2cb5a 100644 --- a/src/commands/features/get.test.ts +++ b/src/commands/features/get.test.ts @@ -70,4 +70,32 @@ describe('features get', () => { .it('passes search param to api', (ctx) => { verifyOutput(JSON.parse(ctx.stdout)) }) + + // Test positional arguments functionality + dvcTest() + .nock(BASE_URL, (api) => + api + .get(`/v2/projects/${projectKey}/features/feature-1`) + .reply(200, mockFeatures[0]) + .get(`/v2/projects/${projectKey}/features/feature-2`) + .reply(200, mockFeatures[1]), + ) + .stdout() + .command([ + 'features get', + 'feature-1', + 'feature-2', + '--project', + projectKey, + ...authFlags, + ]) + .it( + 'fetches multiple features by space-separated positional arguments', + (ctx) => { + expect(JSON.parse(ctx.stdout)).to.deep.equal([ + mockFeatures[0], + mockFeatures[1], + ]) + }, + ) }) diff --git a/src/commands/features/get.ts b/src/commands/features/get.ts index bb21de97b..8eef704f1 100644 --- a/src/commands/features/get.ts +++ b/src/commands/features/get.ts @@ -1,15 +1,27 @@ -import { Flags } from '@oclif/core' +import { Args, Flags } from '@oclif/core' import { fetchFeatures, fetchFeatureByKey } from '../../api/features' import Base from '../base' import { batchRequests } from '../../utils/batchRequests' +import { parseKeysFromArgs } from '../../utils/parseKeysFromArgs' export default class DetailedFeatures extends Base { static hidden = false static description = 'Retrieve Features from the Management API' static examples = [ '<%= config.bin %> <%= command.id %>', + '<%= config.bin %> <%= command.id %> feature-one', + '<%= config.bin %> <%= command.id %> feature-one feature-two', + '<%= config.bin %> <%= command.id %> feature-one,feature-two', '<%= config.bin %> <%= command.id %> --keys=feature-one,feature-two', ] + static args = { + keys: Args.string({ + description: + 'Feature keys to fetch (space-separated or comma-separated)', + required: false, + }), + } + static strict = false static flags = { ...Base.flags, keys: Flags.string({ @@ -29,12 +41,14 @@ export default class DetailedFeatures extends Base { authRequired = true public async run(): Promise { - const { flags } = await this.parse(DetailedFeatures) - const keys = flags['keys']?.split(',') + const { args, argv, flags } = await this.parse(DetailedFeatures) const { project, headless } = flags await this.requireProject(project, headless) + + const keys = parseKeysFromArgs(args, argv, flags) + let features - if (keys) { + if (keys && keys.length > 0) { features = await batchRequests(keys, (key) => fetchFeatureByKey(this.authToken, this.projectKey, key), ) diff --git a/src/commands/variables/get.test.ts b/src/commands/variables/get.test.ts index 80d0297dc..e401800d7 100644 --- a/src/commands/variables/get.test.ts +++ b/src/commands/variables/get.test.ts @@ -83,4 +83,31 @@ describe('variables get', () => { JSON.stringify(mockVariables, null, 2), ) }) + + // Test positional arguments functionality + dvcTest() + .nock(BASE_URL, (api) => + api + .get(`/v1/projects/${projectKey}/variables/variable-1`) + .reply(200, mockVariables[0]) + .get(`/v1/projects/${projectKey}/variables/variable-2`) + .reply(200, mockVariables[1]), + ) + .stdout() + .command([ + 'variables get', + 'variable-1', + 'variable-2', + '--project', + projectKey, + ...authFlags, + ]) + .it( + 'fetches multiple variables by space-separated positional arguments', + (ctx) => { + expect(ctx.stdout).to.contain( + JSON.stringify(mockVariables, null, 2), + ) + }, + ) }) diff --git a/src/commands/variables/get.ts b/src/commands/variables/get.ts index f395da6a3..e82fafb78 100644 --- a/src/commands/variables/get.ts +++ b/src/commands/variables/get.ts @@ -1,10 +1,27 @@ -import { Flags } from '@oclif/core' +import { Args, Flags } from '@oclif/core' import { fetchVariables, fetchVariableByKey } from '../../api/variables' import Base from '../base' import { batchRequests } from '../../utils/batchRequests' +import { parseKeysFromArgs } from '../../utils/parseKeysFromArgs' export default class DetailedVariables extends Base { static hidden = false + static description = 'Retrieve Variables from the Management API' + static examples = [ + '<%= config.bin %> <%= command.id %>', + '<%= config.bin %> <%= command.id %> var-one', + '<%= config.bin %> <%= command.id %> var-one var-two', + '<%= config.bin %> <%= command.id %> var-one,var-two', + '<%= config.bin %> <%= command.id %> --keys=var-one,var-two', + ] + static args = { + keys: Args.string({ + description: + 'Variable keys to fetch (space-separated or comma-separated)', + required: false, + }), + } + static strict = false static flags = { ...Base.flags, keys: Flags.string({ @@ -24,13 +41,14 @@ export default class DetailedVariables extends Base { authRequired = true public async run(): Promise { - const { flags } = await this.parse(DetailedVariables) - const keys = flags['keys']?.split(',') + const { args, argv, flags } = await this.parse(DetailedVariables) const { project, headless } = flags await this.requireProject(project, headless) + const keys = parseKeysFromArgs(args, argv, flags) + let variables - if (keys) { + if (keys && keys.length > 0) { variables = await batchRequests(keys, (key) => fetchVariableByKey(this.authToken, this.projectKey, key), ) diff --git a/src/utils/parseKeysFromArgs.test.ts b/src/utils/parseKeysFromArgs.test.ts new file mode 100644 index 000000000..b66ebc8e5 --- /dev/null +++ b/src/utils/parseKeysFromArgs.test.ts @@ -0,0 +1,18 @@ +import { expect } from '@oclif/test' +import { parseKeysFromArgs } from './parseKeysFromArgs' + +describe('parseKeysFromArgs', () => { + it('should parse positional arguments with proper precedence over flags', () => { + // Test multiple scenarios in one comprehensive test + const result1 = parseKeysFromArgs({}, ['key1', 'key2'], { + keys: 'ignored', + }) + expect(result1).to.deep.equal(['key1', 'key2']) + + const result2 = parseKeysFromArgs({}, ['key1,key2'], {}) + expect(result2).to.deep.equal(['key1', 'key2']) + + const result3 = parseKeysFromArgs({}, [], { keys: 'key1,key2' }) + expect(result3).to.deep.equal(['key1', 'key2']) + }) +}) diff --git a/src/utils/parseKeysFromArgs.ts b/src/utils/parseKeysFromArgs.ts new file mode 100644 index 000000000..56aa13084 --- /dev/null +++ b/src/utils/parseKeysFromArgs.ts @@ -0,0 +1,27 @@ +/** + * Parse keys from command arguments with proper precedence handling + * Supports positional arguments, named args, and flags with comma/space separation + */ +export function parseKeysFromArgs( + args: { keys?: string }, + argv: unknown[], + flags: { keys?: string }, +): string[] | undefined { + // Handle positional arguments - they take precedence over --keys flag + if (argv && argv.length > 0) { + // Collect all positional arguments and split any comma-separated values + return argv.flatMap((arg) => String(arg).split(',')) + } + + if (args.keys) { + // Handle the first positional argument if provided + return args.keys.split(',') + } + + if (flags.keys) { + // Fall back to --keys flag + return flags.keys.split(',') + } + + return undefined +}