From dfbb6c99f0de5b551c5df939920355bafd5b6db2 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Fri, 4 Sep 2026 13:34:24 -0600 Subject: [PATCH 1/4] feat: add --debug-level and --category-level flags to apex run @W-18404446@ Allow users to control the debug log verbosity when executing anonymous Apex. --debug-level sets a predefined SOAP log type (NONE, DEBUGONLY, DB, PROFILING, CALLOUT, DETAIL). --category-level sets individual category levels for fine-grained control (e.g., Apex_code=FINEST, Db=FINE). --- messages/run.md | 28 ++++++++++++++ src/commands/apex/run.ts | 43 ++++++++++++++++++++- test/commands/apex/run.test.ts | 68 ++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 1 deletion(-) diff --git a/messages/run.md b/messages/run.md index d73143fe..1c53b406 100644 --- a/messages/run.md +++ b/messages/run.md @@ -13,6 +13,34 @@ For more information, see "Anonymous Blocks" in the Apex Developer Guide. Path to a local file that contains Apex code. +# flags.debug-level.summary + +Debug level to use for the returned debug log. + +# flags.debug-level.description + +Sets the debug log level for the anonymous Apex execution. Valid values are NONE, DEBUGONLY, DB, PROFILING, CALLOUT, and DETAIL. Defaults to DEBUGONLY if not specified. Mutually exclusive with --category-level. + +# flags.category-level.summary + +Set the log level for a specific log category (format: Category=Level). Can be specified multiple times. + +# flags.category-level.description + +Set individual log category levels for fine-grained control over the debug log. Format: Category=Level (e.g., Apex_code=FINEST). Valid categories: Db, Workflow, Validation, Callout, Apex_code, Apex_profiling, Visualforce, System, Wave, Nba. Valid levels: NONE, ERROR, WARN, INFO, DEBUG, FINE, FINER, FINEST. Can be specified multiple times. Mutually exclusive with --debug-level. + +# invalidCategoryLevel + +Invalid --category-level format "%s". Use Category=Level (e.g., Apex_code=FINEST). + +# invalidCategory + +Invalid category "%s". Valid categories: %s + +# invalidCategoryLevelValue + +Invalid level "%s". Valid levels: %s + # examples - Execute the Apex code that's in the ~/test.apex file in the org with the specified username: diff --git a/src/commands/apex/run.ts b/src/commands/apex/run.ts index 5b42d7cb..6d748436 100644 --- a/src/commands/apex/run.ts +++ b/src/commands/apex/run.ts @@ -14,7 +14,17 @@ * limitations under the License. */ -import { ApexExecuteOptions, ExecuteService } from '@salesforce/apex-node'; +import { + ApexExecuteOptions, + ExecuteService, + LOG_TYPES, + LOG_CATEGORIES, + LOG_CATEGORY_LEVELS, + type DebugCategory, + type LogType, + type LogCategory, + type LogCategoryLevel, +} from '@salesforce/apex-node'; import { Flags, loglevel, @@ -28,6 +38,20 @@ import RunReporter from '../../reporters/runReporter.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-apex', 'run'); +function parseCategoryLevel(input: string): DebugCategory { + const [cat, lvl] = input.split('='); + if (!cat || !lvl) { + throw new SfError(messages.getMessage('invalidCategoryLevel', [input])); + } + if (!LOG_CATEGORIES.includes(cat as LogCategory)) { + throw new SfError(messages.getMessage('invalidCategory', [cat, LOG_CATEGORIES.join(', ')])); + } + if (!LOG_CATEGORY_LEVELS.includes(lvl as LogCategoryLevel)) { + throw new SfError(messages.getMessage('invalidCategoryLevelValue', [lvl, LOG_CATEGORY_LEVELS.join(', ')])); + } + return { category: cat as LogCategory, level: lvl as LogCategoryLevel }; +} + export type ExecuteResult = { compiled: boolean; success: boolean; @@ -56,6 +80,19 @@ export default class Run extends SfCommand { char: 'f', summary: messages.getMessage('flags.file.summary'), }), + 'debug-level': Flags.option({ + char: 'd', + summary: messages.getMessage('flags.debug-level.summary'), + description: messages.getMessage('flags.debug-level.description'), + options: [...LOG_TYPES] as LogType[], + exclusive: ['category-level'], + })(), + 'category-level': Flags.string({ + summary: messages.getMessage('flags.category-level.summary'), + description: messages.getMessage('flags.category-level.description'), + multiple: true, + exclusive: ['debug-level'], + }), }; public async run(): Promise { @@ -63,8 +100,12 @@ export default class Run extends SfCommand { const conn = flags['target-org'].getConnection(flags['api-version']); const exec = new ExecuteService(conn); + const debugCategories = flags['category-level']?.map(parseCategoryLevel); + const execAnonOptions: ApexExecuteOptions = { ...(flags.file ? { apexFilePath: flags.file } : { userInput: true }), + ...(flags['debug-level'] ? { debugLevel: flags['debug-level'] } : {}), + ...(debugCategories?.length ? { debugCategories } : {}), }; const result = await exec.executeAnonymous(execAnonOptions); diff --git a/test/commands/apex/run.test.ts b/test/commands/apex/run.test.ts index 734d22d5..6a0b71c7 100644 --- a/test/commands/apex/run.test.ts +++ b/test/commands/apex/run.test.ts @@ -135,6 +135,74 @@ describe('apex:execute', () => { ]); }); + it('passes debug-level flag to executeAnonymous', async () => { + const file = join('Users', 'test', 'path', 'to', 'file'); + const executeServiceStub = sandboxStub + .stub(ExecuteService.prototype, 'executeAnonymous') + .resolves({ compiled: true, success: true, logs: log }); + + await Run.run(['--file', file, '--debug-level', 'DETAIL']); + + expect(executeServiceStub.args[0]).to.deep.equal([ + { + apexFilePath: file, + debugLevel: 'DETAIL', + }, + ]); + }); + + it('passes category-level flags to executeAnonymous', async () => { + const file = join('Users', 'test', 'path', 'to', 'file'); + const executeServiceStub = sandboxStub + .stub(ExecuteService.prototype, 'executeAnonymous') + .resolves({ compiled: true, success: true, logs: log }); + + await Run.run(['--file', file, '--category-level', 'Apex_code=FINEST', '--category-level', 'Db=FINE']); + + expect(executeServiceStub.args[0]).to.deep.equal([ + { + apexFilePath: file, + debugCategories: [ + { category: 'Apex_code', level: 'FINEST' }, + { category: 'Db', level: 'FINE' }, + ], + }, + ]); + }); + + it('throws on invalid category-level format', async () => { + sandboxStub.stub(ExecuteService.prototype, 'executeAnonymous').resolves({ compiled: true, success: true }); + + try { + await Run.run(['--file', 'test.apex', '--category-level', 'bad-format']); + expect.fail('should have thrown'); + } catch (e) { + expect((e as Error).message).to.include('Invalid --category-level format'); + } + }); + + it('throws on invalid category name', async () => { + sandboxStub.stub(ExecuteService.prototype, 'executeAnonymous').resolves({ compiled: true, success: true }); + + try { + await Run.run(['--file', 'test.apex', '--category-level', 'FakeCategory=FINEST']); + expect.fail('should have thrown'); + } catch (e) { + expect((e as Error).message).to.include('Invalid category'); + } + }); + + it('throws on invalid category level value', async () => { + sandboxStub.stub(ExecuteService.prototype, 'executeAnonymous').resolves({ compiled: true, success: true }); + + try { + await Run.run(['--file', 'test.apex', '--category-level', 'Apex_code=INVALID']); + expect.fail('should have thrown'); + } catch (e) { + expect((e as Error).message).to.include('Invalid level'); + } + }); + it('throws an error when it fails to compile', async () => { sandboxStub.stub(ExecuteService.prototype, 'executeAnonymous').resolves({ compiled: false, From d91d434ea1640d6db4adeab79ce7ce3b5650b0ed Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Fri, 4 Sep 2026 13:46:09 -0600 Subject: [PATCH 2/4] fix: address review findings for debug level flags - Use indexOf('=') for robust key=value parsing (handles extra = segments) - Trim whitespace from parsed category and level values - Deduplicate category-level entries (last-wins when same category repeated) - Add CLI examples for --debug-level and --category-level flags - Add test for duplicate category deduplication --- messages/run.md | 8 ++++++++ src/commands/apex/run.ts | 14 ++++++++++++-- test/commands/apex/run.test.ts | 16 ++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/messages/run.md b/messages/run.md index 1c53b406..632dc182 100644 --- a/messages/run.md +++ b/messages/run.md @@ -55,6 +55,14 @@ Invalid level "%s". Valid levels: %s <%= config.bin %> <%= command.id %> +- Execute with maximum debug log detail: + + <%= config.bin %> <%= command.id %> --file ~/test.apex --debug-level DETAIL + +- Execute with fine-grained control over specific log categories: + + <%= config.bin %> <%= command.id %> --file ~/test.apex --category-level Apex_code=FINEST --category-level Db=FINE + # executeCompileSuccess Compiled successfully. diff --git a/src/commands/apex/run.ts b/src/commands/apex/run.ts index 6d748436..b43108a7 100644 --- a/src/commands/apex/run.ts +++ b/src/commands/apex/run.ts @@ -39,7 +39,12 @@ Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-apex', 'run'); function parseCategoryLevel(input: string): DebugCategory { - const [cat, lvl] = input.split('='); + const eqIndex = input.indexOf('='); + if (eqIndex === -1) { + throw new SfError(messages.getMessage('invalidCategoryLevel', [input])); + } + const cat = input.slice(0, eqIndex).trim(); + const lvl = input.slice(eqIndex + 1).trim(); if (!cat || !lvl) { throw new SfError(messages.getMessage('invalidCategoryLevel', [input])); } @@ -100,7 +105,12 @@ export default class Run extends SfCommand { const conn = flags['target-org'].getConnection(flags['api-version']); const exec = new ExecuteService(conn); - const debugCategories = flags['category-level']?.map(parseCategoryLevel); + const debugCategories = flags['category-level']?.map(parseCategoryLevel).reduce((acc, entry) => { + const idx = acc.findIndex((e) => e.category === entry.category); + if (idx >= 0) acc[idx] = entry; + else acc.push(entry); + return acc; + }, []); const execAnonOptions: ApexExecuteOptions = { ...(flags.file ? { apexFilePath: flags.file } : { userInput: true }), diff --git a/test/commands/apex/run.test.ts b/test/commands/apex/run.test.ts index 6a0b71c7..340f8b40 100644 --- a/test/commands/apex/run.test.ts +++ b/test/commands/apex/run.test.ts @@ -203,6 +203,22 @@ describe('apex:execute', () => { } }); + it('deduplicates category-level entries with last-wins', async () => { + const file = join('Users', 'test', 'path', 'to', 'file'); + const executeServiceStub = sandboxStub + .stub(ExecuteService.prototype, 'executeAnonymous') + .resolves({ compiled: true, success: true, logs: log }); + + await Run.run(['--file', file, '--category-level', 'Apex_code=DEBUG', '--category-level', 'Apex_code=FINEST']); + + expect(executeServiceStub.args[0]).to.deep.equal([ + { + apexFilePath: file, + debugCategories: [{ category: 'Apex_code', level: 'FINEST' }], + }, + ]); + }); + it('throws an error when it fails to compile', async () => { sandboxStub.stub(ExecuteService.prototype, 'executeAnonymous').resolves({ compiled: false, From 28c675210be91c5ebc4e170ca67d0d0fe2d357fd Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Tue, 8 Sep 2026 09:05:15 -0600 Subject: [PATCH 3/4] docs: add 'All' category to --category-level description --- messages/run.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/run.md b/messages/run.md index 632dc182..25ef02a9 100644 --- a/messages/run.md +++ b/messages/run.md @@ -27,7 +27,7 @@ Set the log level for a specific log category (format: Category=Level). Can be s # flags.category-level.description -Set individual log category levels for fine-grained control over the debug log. Format: Category=Level (e.g., Apex_code=FINEST). Valid categories: Db, Workflow, Validation, Callout, Apex_code, Apex_profiling, Visualforce, System, Wave, Nba. Valid levels: NONE, ERROR, WARN, INFO, DEBUG, FINE, FINER, FINEST. Can be specified multiple times. Mutually exclusive with --debug-level. +Set individual log category levels for fine-grained control over the debug log. Format: Category=Level (e.g., Apex_code=FINEST). Valid categories: Db, Workflow, Validation, Callout, Apex_code, Apex_profiling, Visualforce, System, Wave, Nba, All. Valid levels: NONE, ERROR, WARN, INFO, DEBUG, FINE, FINER, FINEST. Can be specified multiple times. Mutually exclusive with --debug-level. # invalidCategoryLevel From 69f17e6df7b090883976ab120645c9325a59cf13 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Fri, 11 Sep 2026 09:10:53 -0600 Subject: [PATCH 4/4] chore: bump apx-node --- command-snapshot.json | 7 ++++--- package.json | 2 +- yarn.lock | 38 +++++++++++++++++++------------------- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/command-snapshot.json b/command-snapshot.json index 8ac0d7a2..7459571d 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -48,8 +48,8 @@ "alias": ["force:apex:execute"], "command": "apex:run", "flagAliases": ["apexcodefile", "apiversion", "targetusername", "u"], - "flagChars": ["f", "o"], - "flags": ["api-version", "file", "flags-dir", "json", "loglevel", "target-org"], + "flagChars": ["d", "f", "o"], + "flags": ["api-version", "category-level", "debug-level", "file", "flags-dir", "json", "loglevel", "target-org"], "plugin": "@salesforce/plugin-apex" }, { @@ -78,8 +78,8 @@ "flags-dir", "json", "loglevel", - "poll-interval", "output-dir", + "poll-interval", "result-format", "suite-names", "synchronous", @@ -99,6 +99,7 @@ "plugin": "@salesforce/plugin-apex" }, { + "alias": [], "command": "logic:get:test", "flagAliases": [ "apiversion", diff --git a/package.json b/package.json index 2befe749..8487ee23 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "enableO11y": true, "o11yUploadEndpoint": "https://794testsite.my.site.com/byolwr/webruntime/log/metrics", "dependencies": { - "@salesforce/apex-node": "^9.0.0", + "@salesforce/apex-node": "^9.1.0", "@salesforce/core": "^9.0.0", "@salesforce/kit": "^4.0.0", "@salesforce/sf-plugins-core": "^13.0.0", diff --git a/yarn.lock b/yarn.lock index f9bcf5cc..25b148d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1093,10 +1093,10 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@jsforce/jsforce-node@^3.10.17": - version "3.10.19" - resolved "https://registry.yarnpkg.com/@jsforce/jsforce-node/-/jsforce-node-3.10.19.tgz#ccbc539c12f4f7dff9cfdcc6cfb8f07bd840f731" - integrity sha512-k7i2Tntu1fLvkMtRcKDFU64/Fr2M692ECtbwIGX6hcOh5mj+jrMa1tlvcdwffxAMl+lPYCXnY2bjErxWmP84zA== +"@jsforce/jsforce-node@^3.10.17", "@jsforce/jsforce-node@^3.10.24": + version "3.10.25" + resolved "https://registry.yarnpkg.com/@jsforce/jsforce-node/-/jsforce-node-3.10.25.tgz#19a457a8d0b5217188576507139134cfae836eef" + integrity sha512-L4GfTOzmGBypjG1O7aTxwbk4DTKitU7yvnnqQE7wypFyEyHH9VR6cVZxi4+Gfk7cmItHRHAV7MyBTFfrP5DaHA== dependencies: "@sindresorhus/is" "^4" base64url "^3.0.1" @@ -1295,12 +1295,12 @@ "@pnpm/network.ca-file" "^1.0.1" config-chain "^1.1.11" -"@salesforce/apex-node@^9.0.0": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@salesforce/apex-node/-/apex-node-9.0.0.tgz#8528d8627cf0bf224c4bcfcb09ced765dc6e71ca" - integrity sha512-OwOnUxaec9tBcWG2lqg8e7qmazpgzd4oUZuyaw/xbnz21pQAztfn2gAlZNGGnunAKtceyarO9iLygt/ebXJC/g== +"@salesforce/apex-node@^9.1.0": + version "9.1.0" + resolved "https://registry.yarnpkg.com/@salesforce/apex-node/-/apex-node-9.1.0.tgz#419407f9b179251be81429bb5b8d629dc2dede1c" + integrity sha512-MY5iEWrWMJV7Rn5yxhYMFQFTsQ9uxEOSiU5RHO8aIWbEARkQ7aiev4Hy55nBxSkE9LuKQMHNZLFwiAvHfuj1Zw== dependencies: - "@salesforce/core" "^9.0.0" + "@salesforce/core" "^9.1.10" "@salesforce/kit" "^4.0.0" "@types/istanbul-reports" "^3.0.4" fast-glob "^3.3.2" @@ -1351,14 +1351,14 @@ ts-retry-promise "^0.8.1" zod "^4.1.12" -"@salesforce/core@^9.0.0": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-9.0.0.tgz#bf7a2816a322b8febc5349f5a1c884b8be073b06" - integrity sha512-sL4sr8MXcdsHZJr0bacMY0ZJmbPwBEvJudDIlKxKdQu/62d1aNBvACPFfL32QJdu2sOkEpgF3CX/0znkU43g2g== +"@salesforce/core@^9.0.0", "@salesforce/core@^9.1.10": + version "9.1.11" + resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-9.1.11.tgz#dd2226ac1d67434575aac92eaa3ad9c1a12dcc2e" + integrity sha512-EE4oGYsoKThank4dQ1mVIzJjS9yP6C6s7JzKDx+ki8AFAxgpposzLc6GYwZZq1tS0XaOfBL2x+8RrgEgZvXwnw== dependencies: - "@jsforce/jsforce-node" "^3.10.17" + "@jsforce/jsforce-node" "^3.10.24" "@salesforce/kit" "^4.0.0" - "@salesforce/ts-types" "^3.0.0" + "@salesforce/ts-types" "^3.2.0" ajv "^8.18.0" change-case "^4.1.2" fast-levenshtein "^3.0.0" @@ -1487,10 +1487,10 @@ resolved "https://registry.yarnpkg.com/@salesforce/ts-types/-/ts-types-2.0.12.tgz#60420622812a7ec7e46d220667bc29b42dc247ff" integrity sha512-BIJyduJC18Kc8z+arUm5AZ9VkPRyw1KKAm+Tk+9LT99eOzhNilyfKzhZ4t+tG2lIGgnJpmytZfVDZ0e2kFul8g== -"@salesforce/ts-types@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@salesforce/ts-types/-/ts-types-3.0.0.tgz#375b13e92f594bc9d0a09716d77fc753bd31e6a6" - integrity sha512-lTCoI1NtQWiMDyn7B9N/CfWK9WmSpRnbYWqKTZYstn3mZ2DWSYwJXTh5Oh2wAAS9LAD/vaoU3F/TrV3q7wfnYw== +"@salesforce/ts-types@^3.0.0", "@salesforce/ts-types@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@salesforce/ts-types/-/ts-types-3.2.0.tgz#9e0393cab10d763b89562f0f94515e3cf1efbbe7" + integrity sha512-3xdKt4nlthDC8nCHVWpVDwM5mmPlk8kDyJ1E3dWMk7FLRdQqhLgaHZgS+AMqcowWiKUC1l1d5YIhGhQM7dw3Bw== "@shikijs/core@1.29.2": version "1.29.2"