From 078dcb2bbc6ed2420a7882872f2ef6996de73396 Mon Sep 17 00:00:00 2001 From: Satyammittal1011 Date: Fri, 14 Aug 2026 15:04:13 +0530 Subject: [PATCH 1/3] feat: add Brevo Function commands with smoke tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `brevo function list` and `brevo function get ` commands for managing Brevo Functions, plus the "Brevo Function" app type choice in `brevo app create`. - New service (src/services/function.ts) with list, draft list, and get - New command handlers (src/commands/function/list.ts, get.ts) - Register functionCommandGroup in definitions.ts and bin/index.ts - Add Function commands section to root help screen - Add brevo_function template flag for app-config.json rendering - Add Brevo Function app type to interactive create prompt (private only) - Unit tests for service, list command, and get command (23 tests) - Command registration tests in definitions.test.ts (4 tests) - Help formatting tests updated for functionCommandGroup - New smoke suite (scripts/smoke/function.ts) exercising list, list --draft, get, and get-not-found against a real account — opt-in via `yarn smoke --suite=function` Co-Authored-By: Claude Opus 4.6 --- scripts/smoke-test.ts | 7 +- scripts/smoke/function.ts | 132 ++++ src/__tests__/commands/app/create.test.ts | 8 +- src/__tests__/commands/definitions.test.ts | 32 +- src/__tests__/commands/function/get.test.ts | 135 ++++ src/__tests__/commands/function/list.test.ts | 219 +++++++ src/__tests__/lib/help.test.ts | 16 +- src/__tests__/services/function.test.ts | 119 ++++ src/bin/index.ts | 9 +- src/commands/app/create.ts | 33 +- src/commands/app/project-writer.ts | 5 + src/commands/definitions.ts | 32 + src/commands/function/get.ts | 54 ++ src/commands/function/list.ts | 84 +++ src/container.ts | 2 + src/lang/en.ts | 17 +- src/lib/constants.ts | 4 + src/lib/help.ts | 4 + src/services/function.ts | 23 + src/templates/index.ts | 11 +- src/types.ts | 45 ++ tsconfig.eslint.json | 3 +- yarn.lock | 653 ++++++++----------- 23 files changed, 1243 insertions(+), 404 deletions(-) create mode 100644 scripts/smoke/function.ts create mode 100644 src/__tests__/commands/function/get.test.ts create mode 100644 src/__tests__/commands/function/list.test.ts create mode 100644 src/__tests__/services/function.test.ts create mode 100644 src/commands/function/get.ts create mode 100644 src/commands/function/list.ts create mode 100644 src/services/function.ts diff --git a/scripts/smoke-test.ts b/scripts/smoke-test.ts index 4154666..0a40657 100644 --- a/scripts/smoke-test.ts +++ b/scripts/smoke-test.ts @@ -48,13 +48,17 @@ import { import { privateAppSuite } from './smoke/private-app'; import { publicAppSuite } from './smoke/public-app'; import { initWizardSuite } from './smoke/init-wizard'; +import { functionSuite } from './smoke/function'; // Suite registry. `--suite=` picks from these; the init wizard is -// opt-in because it drives interactive prompts through scripted stdin. +// opt-in because it drives interactive prompts through scripted stdin, and the +// function suite is opt-in because it exercises read-only endpoints that may +// return empty results on a fresh account. const SUITES: Record = { private: privateAppSuite, public: publicAppSuite, init: initWizardSuite, + function: functionSuite, }; const DEFAULT_SUITES = ['private', 'public']; @@ -169,6 +173,7 @@ Flags: private private-app lifecycle + client guardrails public public-app submission/review lifecycle init 'brevo app init' wizard (interactive, opt-in) + function Brevo Function list/get commands (read-only, opt-in) all every suite Default: private,public --with-init Append the init suite (same as adding 'init'). diff --git a/scripts/smoke/function.ts b/scripts/smoke/function.ts new file mode 100644 index 0000000..b5fd1bb --- /dev/null +++ b/scripts/smoke/function.ts @@ -0,0 +1,132 @@ +/* + * Brevo Function commands: list -> list --draft -> get -> get (404). + * + * Read-only — no apps are created or deleted. The suite validates response shape + * and error handling, not data content (the account may have zero functions). + */ + +import { + State, + Suite, + brevoCmd, + exec, + execOrThrow, + must, + parseJson, +} from './core'; + +function stepFunctionList(state: State): string { + const r = execOrThrow(brevoCmd(state), ['function', 'list', '--json'], state); + const parsed = parseJson>(r.stdout); + + // The response must carry the list shape regardless of how many functions exist. + must( + Array.isArray(parsed.functions), + `function list --json: "functions" is not an array: ${JSON.stringify(parsed).slice(0, 200)}`, + ); + must( + typeof parsed.total === 'number', + `function list --json: "total" is not a number: ${JSON.stringify(parsed).slice(0, 200)}`, + ); + must( + typeof parsed.max === 'number', + `function list --json: "max" is not a number: ${JSON.stringify(parsed).slice(0, 200)}`, + ); + + const functions = parsed.functions as Array>; + + // If there are functions, spot-check the first one's shape. + const first = functions[0]; + if (first) { + must(typeof first.id === 'string', 'first function missing "id"'); + must(typeof first.name === 'string', 'first function missing "name"'); + must(typeof first.formula === 'string', 'first function missing "formula"'); + } + + // Store the first function ID for the get step. + state._functionId = first ? String(first.id) : null; + + return `${functions.length} function(s), total ${parsed.total} / max ${parsed.max}`; +} + +function stepFunctionListDraft(state: State): string { + const r = execOrThrow(brevoCmd(state), ['function', 'list', '--draft', '--json'], state); + const parsed = parseJson>(r.stdout); + + must( + Array.isArray(parsed.drafts), + `function list --draft --json: "drafts" is not an array: ${JSON.stringify(parsed).slice(0, 200)}`, + ); + must( + typeof parsed.total === 'number', + `function list --draft --json: "total" is not a number`, + ); + + const drafts = parsed.drafts as Array>; + + const firstDraft = drafts[0]; + if (firstDraft) { + must(typeof firstDraft.id === 'string', 'first draft missing "id"'); + must(typeof firstDraft.formula === 'string', 'first draft missing "formula"'); + } + + return `${drafts.length} draft(s), total ${parsed.total}`; +} + +function stepFunctionGet(state: State): string { + const id = state._functionId; + if (!id) { + return 'skipped — no functions on this account to get'; + } + + const r = execOrThrow(brevoCmd(state), ['function', 'get', id, '--json'], state); + const parsed = parseJson>(r.stdout); + + must(parsed.id === id, `function get: returned id "${parsed.id}", expected "${id}"`); + must(typeof parsed.name === 'string', 'function get: missing "name"'); + must(typeof parsed.formula === 'string', 'function get: missing "formula"'); + must(typeof parsed.version === 'number', 'function get: missing "version"'); + must(typeof parsed.is_active === 'boolean', 'function get: missing "is_active"'); + + return `got function "${parsed.name}" (${id})`; +} + +function stepFunctionGetNotFound(state: State): string { + // A nonexistent ID should exit 0 with a JSON error body — the command handles + // 404 gracefully rather than throwing. + const fakeId = 'brevo-cli-smoke-nonexistent-fn'; + const r = exec(brevoCmd(state), ['function', 'get', fakeId, '--json'], state); + + must( + r.exitCode === 0, + `function get (404) exited ${r.exitCode}, expected 0: ${(r.stderr || r.stdout).slice(0, 200)}`, + ); + + const parsed = parseJson>(r.stdout); + must( + parsed.error === 'not_found', + `function get (404): expected error "not_found", got ${JSON.stringify(parsed.error)}`, + ); + + return `404 handled gracefully for "${fakeId}"`; +} + +// Extend State with a temporary field for the function ID found during list. +// This avoids changing the shared State interface — the field is set and read +// only within this suite. +declare module './core' { + interface State { + _functionId?: string | null; + } +} + +export const functionSuite: Suite = { + name: 'function', + description: 'Brevo Function commands — list, list --draft, get, get (404)', + steps: [ + ['Function list', stepFunctionList], + ['Function list (draft)', stepFunctionListDraft], + ['Function get', stepFunctionGet], + ['Function get (not found)', stepFunctionGetNotFound], + ], +}; diff --git a/src/__tests__/commands/app/create.test.ts b/src/__tests__/commands/app/create.test.ts index c86e82d..e4b196d 100644 --- a/src/__tests__/commands/app/create.test.ts +++ b/src/__tests__/commands/app/create.test.ts @@ -319,7 +319,7 @@ describe('app/create', () => { questionNamed(name).choices.map((choice: { value: string }) => choice.value); expect(valuesOf('distribution')).toEqual(['private', 'public']); - expect(valuesOf('appType')).toEqual(['oauth', 'ui']); + expect(valuesOf('appType')).toEqual(['oauth', 'ui', 'function']); }); describe('feature scaffolding', () => { @@ -2540,10 +2540,14 @@ describe('app/create', () => { // assertion below is a `not.toContain`, which an unnoticed indent would satisfy // vacuously, quietly turning the gate's own test green for the wrong reason. const labels = appTypeQuestion.choices.map((choice: { name: string }) => choice.name.trim()); - expect(labels).toEqual([messages.APP_CREATE_APP_TYPE_OAUTH]); + expect(labels).toEqual([ + messages.APP_CREATE_APP_TYPE_OAUTH, + messages.APP_CREATE_APP_TYPE_FUNCTION, + ]); expect(labels).not.toContain(messages.APP_CREATE_APP_TYPE_UI); expect(appTypeQuestion.choices.map((choice: { value: string }) => choice.value)).toEqual([ 'oauth', + 'function', ]); const payload = (appService.createApp as jest.Mock).mock.calls[0][0]; diff --git a/src/__tests__/commands/definitions.test.ts b/src/__tests__/commands/definitions.test.ts index 422db26..f652e8e 100644 --- a/src/__tests__/commands/definitions.test.ts +++ b/src/__tests__/commands/definitions.test.ts @@ -1,4 +1,4 @@ -import { appCommandGroup } from '../../commands/definitions'; +import { appCommandGroup, functionCommandGroup } from '../../commands/definitions'; describe('appCommandGroup', () => { it('registers the available-scopes command', () => { @@ -13,3 +13,33 @@ describe('appCommandGroup', () => { expect(flags).toContain('--json'); }); }); + +describe('functionCommandGroup', () => { + it('registers list and get subcommands', () => { + const names = functionCommandGroup.commands.map((c) => c.name); + expect(names).toContain('list'); + expect(names).toContain('get'); + }); + + it('list command supports --json and --draft', () => { + const cmd = functionCommandGroup.commands.find((c) => c.name === 'list'); + expect(cmd).toBeDefined(); + const flags = (cmd!.options ?? []).map((o) => o.flags); + expect(flags).toContain('--json'); + expect(flags).toContain('--draft'); + }); + + it('get command supports --json', () => { + const cmd = functionCommandGroup.commands.find((c) => c.name === 'get'); + expect(cmd).toBeDefined(); + const flags = (cmd!.options ?? []).map((o) => o.flags); + expect(flags).toContain('--json'); + }); + + it('get command takes an argument', () => { + const cmd = functionCommandGroup.commands.find((c) => c.name === 'get'); + expect(cmd).toBeDefined(); + expect(cmd!.arguments).toBeDefined(); + expect(cmd!.arguments!.some((a) => a.name.includes('id'))).toBe(true); + }); +}); diff --git a/src/__tests__/commands/function/get.test.ts b/src/__tests__/commands/function/get.test.ts new file mode 100644 index 0000000..1aab270 --- /dev/null +++ b/src/__tests__/commands/function/get.test.ts @@ -0,0 +1,135 @@ +import { getFunctionCommand } from '../../../commands/function/get'; +import { ApiError } from '../../../lib/errors'; + +jest.mock('../../../container', () => ({ + functionService: { + fetchFunction: jest.fn(), + }, +})); + +import { functionService } from '../../../container'; + +describe('function/get', () => { + let stdoutSpy: jest.SpyInstance; + + beforeEach(() => { + stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true); + jest.clearAllMocks(); + }); + + afterEach(() => { + stdoutSpy.mockRestore(); + }); + + const sampleFunction = { + id: 'fn-001', + name: 'Score Leads', + description: 'Scores leads based on activity', + explanation: 'Uses engagement data', + formula: 'SUM(clicks) * 10', + category: 'scoring', + version: 1, + is_active: true, + is_global: false, + created_at: '2026-01-01T00:00:00Z', + updated_at: '2026-01-01T00:00:00Z', + last_recalculated_at: '2026-01-15T00:00:00Z', + }; + + describe('getFunctionCommand', () => { + it('should display all function details for a valid ID', async () => { + (functionService.fetchFunction as jest.Mock).mockResolvedValue(sampleFunction); + + await getFunctionCommand({ id: 'fn-001', json: false }); + + const output = stdoutSpy.mock.calls.map((c: [string]) => c[0]).join(''); + expect(output).toContain('Score Leads'); + expect(output).toContain('fn-001'); + expect(output).toContain('active'); + expect(output).toContain('Scores leads based on activity'); + expect(output).toContain('Uses engagement data'); + expect(output).toContain('SUM(clicks) * 10'); + expect(output).toContain('scoring'); + expect(output).toContain('2026-01-01T00:00:00Z'); + expect(output).toContain('2026-01-15T00:00:00Z'); + expect(functionService.fetchFunction).toHaveBeenCalledWith('fn-001'); + }); + + it('should show inactive status for a disabled function', async () => { + (functionService.fetchFunction as jest.Mock).mockResolvedValue({ + ...sampleFunction, + is_active: false, + }); + + await getFunctionCommand({ id: 'fn-001', json: false }); + + const output = stdoutSpy.mock.calls.map((c: [string]) => c[0]).join(''); + expect(output).toContain('inactive'); + }); + + it('should omit optional fields when absent', async () => { + const { category, last_recalculated_at, ...minimal } = sampleFunction; + (functionService.fetchFunction as jest.Mock).mockResolvedValue(minimal); + + await getFunctionCommand({ id: 'fn-001', json: false }); + + const output = stdoutSpy.mock.calls.map((c: [string]) => c[0]).join(''); + expect(output).not.toContain('Category'); + expect(output).not.toContain('Recalculated'); + }); + + it('should output JSON when --json is set', async () => { + (functionService.fetchFunction as jest.Mock).mockResolvedValue(sampleFunction); + + await getFunctionCommand({ id: 'fn-001', json: true }); + + const output = stdoutSpy.mock.calls[0][0]; + const parsed = JSON.parse(output); + expect(parsed.id).toBe('fn-001'); + expect(parsed.name).toBe('Score Leads'); + expect(parsed.formula).toBe('SUM(clicks) * 10'); + }); + + it('should show not-found message on 404', async () => { + (functionService.fetchFunction as jest.Mock).mockRejectedValue( + new ApiError('Not found', 404), + ); + + await getFunctionCommand({ id: 'fn-999', json: false }); + + const output = stdoutSpy.mock.calls.map((c: [string]) => c[0]).join(''); + expect(output).toContain('Brevo Function "fn-999" not found'); + }); + + it('should output JSON error on 404 with --json', async () => { + (functionService.fetchFunction as jest.Mock).mockRejectedValue( + new ApiError('Not found', 404), + ); + + await getFunctionCommand({ id: 'fn-999', json: true }); + + const output = stdoutSpy.mock.calls[0][0]; + const parsed = JSON.parse(output); + expect(parsed.error).toBe('not_found'); + expect(parsed.message).toContain('fn-999'); + }); + + it('should propagate non-404 API errors', async () => { + (functionService.fetchFunction as jest.Mock).mockRejectedValue( + new ApiError('Server error', 500), + ); + + await expect(getFunctionCommand({ id: 'fn-001', json: false })).rejects.toThrow( + 'Server error', + ); + }); + + it('should propagate generic errors', async () => { + (functionService.fetchFunction as jest.Mock).mockRejectedValue(new Error('Network error')); + + await expect(getFunctionCommand({ id: 'fn-001', json: false })).rejects.toThrow( + 'Network error', + ); + }); + }); +}); diff --git a/src/__tests__/commands/function/list.test.ts b/src/__tests__/commands/function/list.test.ts new file mode 100644 index 0000000..450d48b --- /dev/null +++ b/src/__tests__/commands/function/list.test.ts @@ -0,0 +1,219 @@ +import { listFunctionCommand } from '../../../commands/function/list'; + +jest.mock('../../../container', () => ({ + functionService: { + fetchFunctionList: jest.fn(), + fetchDraftFunctionList: jest.fn(), + }, +})); + +import { functionService } from '../../../container'; + +describe('function/list', () => { + let stdoutSpy: jest.SpyInstance; + + beforeEach(() => { + stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true); + jest.clearAllMocks(); + }); + + afterEach(() => { + stdoutSpy.mockRestore(); + }); + + describe('listFunctionCommand', () => { + it('should display functions with total/max count', async () => { + (functionService.fetchFunctionList as jest.Mock).mockResolvedValue({ + functions: [ + { + id: 'fn-001', + name: 'Score Leads', + description: 'Scores leads based on activity', + explanation: 'Uses engagement data', + formula: 'SUM(clicks) * 10', + version: 1, + is_active: true, + is_global: false, + created_at: '2026-01-01T00:00:00Z', + updated_at: '2026-01-01T00:00:00Z', + }, + { + id: 'fn-002', + name: 'Churn Risk', + description: 'Predicts churn risk', + explanation: 'Inactivity metric', + formula: 'DAYS_SINCE(last_open) > 30', + version: 2, + is_active: false, + is_global: true, + created_at: '2026-02-01T00:00:00Z', + updated_at: '2026-02-01T00:00:00Z', + }, + ], + total: 2, + max: 7, + limit: 50, + offset: 0, + has_more: false, + }); + + await listFunctionCommand({ json: false }); + + const output = stdoutSpy.mock.calls.map((c: [string]) => c[0]).join(''); + expect(output).toContain('Score Leads'); + expect(output).toContain('ID: fn-001'); + expect(output).toContain('active'); + expect(output).toContain('SUM(clicks) * 10'); + expect(output).toContain('Churn Risk'); + expect(output).toContain('ID: fn-002'); + expect(output).toContain('inactive'); + expect(output).toContain('Total: 2 / 7'); + }); + + it('should show empty message when no functions exist', async () => { + (functionService.fetchFunctionList as jest.Mock).mockResolvedValue({ + functions: [], + total: 0, + max: 7, + limit: 50, + offset: 0, + has_more: false, + }); + + await listFunctionCommand({ json: false }); + + const output = stdoutSpy.mock.calls.map((c: [string]) => c[0]).join(''); + expect(output).toContain('No Brevo Functions found'); + }); + + it('should output JSON when --json is set', async () => { + const response = { + functions: [ + { + id: 'fn-001', + name: 'Score Leads', + description: 'Scores leads', + explanation: 'Uses data', + formula: 'SUM(clicks)', + version: 1, + is_active: true, + is_global: false, + created_at: '2026-01-01T00:00:00Z', + updated_at: '2026-01-01T00:00:00Z', + }, + ], + total: 1, + max: 7, + limit: 50, + offset: 0, + has_more: false, + }; + (functionService.fetchFunctionList as jest.Mock).mockResolvedValue(response); + + await listFunctionCommand({ json: true }); + + const output = stdoutSpy.mock.calls[0][0]; + const parsed = JSON.parse(output); + expect(parsed.functions).toHaveLength(1); + expect(parsed.functions[0].id).toBe('fn-001'); + expect(parsed.total).toBe(1); + expect(parsed.max).toBe(7); + }); + + it('should handle API errors', async () => { + (functionService.fetchFunctionList as jest.Mock).mockRejectedValue( + new Error('Network error'), + ); + + await expect(listFunctionCommand({ json: false })).rejects.toThrow('Network error'); + }); + + it('should tolerate a response with missing functions array', async () => { + (functionService.fetchFunctionList as jest.Mock).mockResolvedValue({ + total: 0, + max: 7, + limit: 50, + offset: 0, + has_more: false, + }); + + await listFunctionCommand({ json: false }); + + const output = stdoutSpy.mock.calls.map((c: [string]) => c[0]).join(''); + expect(output).toContain('No Brevo Functions found'); + }); + + it('should display draft functions when --draft is set', async () => { + (functionService.fetchDraftFunctionList as jest.Mock).mockResolvedValue({ + drafts: [ + { + id: 'draft-001', + description: 'A draft function', + explanation: 'Draft explanation', + formula: 'X + 1', + created_at: '2026-01-01T00:00:00Z', + expires_at: '2026-01-02T00:00:00Z', + }, + ], + total: 1, + limit: 50, + offset: 0, + has_more: false, + }); + + await listFunctionCommand({ json: false, draft: true }); + + expect(functionService.fetchDraftFunctionList).toHaveBeenCalled(); + const output = stdoutSpy.mock.calls.map((c: [string]) => c[0]).join(''); + expect(output).toContain('draft Brevo Functions'); + expect(output).toContain('draft-001'); + expect(output).toContain('A draft function'); + expect(output).toContain('X + 1'); + expect(output).toContain('Expires'); + expect(output).toContain('Total: 1'); + }); + + it('should show draft empty message when --draft returns no drafts', async () => { + (functionService.fetchDraftFunctionList as jest.Mock).mockResolvedValue({ + drafts: [], + total: 0, + limit: 50, + offset: 0, + has_more: false, + }); + + await listFunctionCommand({ json: false, draft: true }); + + const output = stdoutSpy.mock.calls.map((c: [string]) => c[0]).join(''); + expect(output).toContain('No draft Brevo Functions found'); + }); + + it('should output draft JSON when --draft --json is set', async () => { + const response = { + drafts: [ + { + id: 'draft-001', + description: 'A draft', + explanation: 'Explanation', + formula: 'X + 1', + created_at: '2026-01-01T00:00:00Z', + expires_at: '2026-01-02T00:00:00Z', + }, + ], + total: 1, + limit: 50, + offset: 0, + has_more: false, + }; + (functionService.fetchDraftFunctionList as jest.Mock).mockResolvedValue(response); + + await listFunctionCommand({ json: true, draft: true }); + + const output = stdoutSpy.mock.calls[0][0]; + const parsed = JSON.parse(output); + expect(parsed.drafts).toHaveLength(1); + expect(parsed.drafts[0].id).toBe('draft-001'); + expect(parsed.total).toBe(1); + }); + }); +}); diff --git a/src/__tests__/lib/help.test.ts b/src/__tests__/lib/help.test.ts index a0155a7..2dddf87 100644 --- a/src/__tests__/lib/help.test.ts +++ b/src/__tests__/lib/help.test.ts @@ -1,7 +1,12 @@ import { Command } from 'commander'; import { createHelpFormatter } from '../../lib/help'; import { registerAll } from '../../lib/command-registry'; -import { topLevelCommands, appCommandGroup, skillCommandGroup } from '../../commands/definitions'; +import { + topLevelCommands, + appCommandGroup, + skillCommandGroup, + functionCommandGroup, +} from '../../commands/definitions'; // Build the real command tree the same way bin/index.ts does, so these assertions // run against the actual registered options rather than a stand-in. @@ -13,7 +18,11 @@ function buildProgram(): Command { .version('0.0.0-test') .option('--debug', 'Enable debug logging') .configureHelp({ formatHelp: createHelpFormatter(program) }); - registerAll(program, topLevelCommands, [appCommandGroup, skillCommandGroup]); + registerAll(program, topLevelCommands, [ + appCommandGroup, + skillCommandGroup, + functionCommandGroup, + ]); return program; } @@ -52,6 +61,7 @@ describe('help formatting', () => { expect(out).toContain('App-deployment commands (UI apps only):'); expect(out).toContain('App-review commands (public apps only):'); expect(out).toContain('Skill commands:'); + expect(out).toContain('Function commands:'); expect(out).toContain('Run `brevo --help` for details on a specific command.'); }); @@ -100,7 +110,7 @@ describe('help formatting', () => { it('gives every registered subcommand its own usage line', () => { const program = buildProgram(); - const groups = [appCommandGroup, skillCommandGroup]; + const groups = [appCommandGroup, skillCommandGroup, functionCommandGroup]; for (const group of groups) { for (const cmd of group.commands) { diff --git a/src/__tests__/services/function.test.ts b/src/__tests__/services/function.test.ts new file mode 100644 index 0000000..6bfe559 --- /dev/null +++ b/src/__tests__/services/function.test.ts @@ -0,0 +1,119 @@ +import { ApiClient } from '../../api/client'; +import { createFunctionService } from '../../services/function'; + +function createMockClient() { + return { + get: jest.fn(), + post: jest.fn(), + patch: jest.fn(), + put: jest.fn(), + delete: jest.fn(), + getWithKey: jest.fn(), + setOnAuthFailure: jest.fn(), + } as unknown as ApiClient; +} + +describe('services/function', () => { + let mockClient: ApiClient; + let service: ReturnType; + + beforeEach(() => { + mockClient = createMockClient(); + service = createFunctionService(mockClient); + }); + + describe('fetchFunctionList', () => { + it('should call client.get with the Brevo Functions endpoint', async () => { + const response = { + functions: [ + { + id: 'fn-001', + name: 'Score Leads', + description: 'Scores leads', + explanation: 'Uses data', + formula: 'SUM(clicks)', + version: 1, + is_active: true, + is_global: false, + created_at: '2026-01-01T00:00:00Z', + updated_at: '2026-01-01T00:00:00Z', + }, + ], + total: 1, + max: 7, + limit: 50, + offset: 0, + has_more: false, + }; + (mockClient.get as jest.Mock).mockResolvedValue(response); + + const result = await service.fetchFunctionList(); + + expect(mockClient.get).toHaveBeenCalledWith('/v3/dp-functions/functions?limit=50&offset=0'); + expect(result).toEqual(response); + }); + + it('should propagate API errors', async () => { + (mockClient.get as jest.Mock).mockRejectedValue(new Error('Forbidden')); + + await expect(service.fetchFunctionList()).rejects.toThrow('Forbidden'); + }); + }); + + describe('fetchDraftFunctionList', () => { + it('should call client.get with draft=true query param', async () => { + const response = { drafts: [], total: 0 }; + (mockClient.get as jest.Mock).mockResolvedValue(response); + + const result = await service.fetchDraftFunctionList(); + + expect(mockClient.get).toHaveBeenCalledWith( + '/v3/dp-functions/functions?limit=50&offset=0&draft=true', + ); + expect(result).toEqual(response); + }); + + it('should propagate API errors', async () => { + (mockClient.get as jest.Mock).mockRejectedValue(new Error('Forbidden')); + + await expect(service.fetchDraftFunctionList()).rejects.toThrow('Forbidden'); + }); + }); + + describe('fetchFunction', () => { + it('should call client.get with the single-function endpoint', async () => { + const response = { + id: 'fn-001', + name: 'Score Leads', + description: 'Scores leads', + explanation: 'Uses data', + formula: 'SUM(clicks)', + version: 1, + is_active: true, + is_global: false, + created_at: '2026-01-01T00:00:00Z', + updated_at: '2026-01-01T00:00:00Z', + }; + (mockClient.get as jest.Mock).mockResolvedValue(response); + + const result = await service.fetchFunction('fn-001'); + + expect(mockClient.get).toHaveBeenCalledWith('/v3/dp-functions/functions/fn-001'); + expect(result).toEqual(response); + }); + + it('should encode the function ID in the URL', async () => { + (mockClient.get as jest.Mock).mockResolvedValue({}); + + await service.fetchFunction('fn/special id'); + + expect(mockClient.get).toHaveBeenCalledWith('/v3/dp-functions/functions/fn%2Fspecial%20id'); + }); + + it('should propagate API errors', async () => { + (mockClient.get as jest.Mock).mockRejectedValue(new Error('Not found')); + + await expect(service.fetchFunction('fn-999')).rejects.toThrow('Not found'); + }); + }); +}); diff --git a/src/bin/index.ts b/src/bin/index.ts index e1bd157..d199a1b 100644 --- a/src/bin/index.ts +++ b/src/bin/index.ts @@ -17,7 +17,12 @@ import { stopActiveSpinner } from '../lib/ui'; import { AccountResponse } from '../types'; import { client } from '../container'; import { registerAll } from '../lib/command-registry'; -import { topLevelCommands, appCommandGroup, skillCommandGroup } from '../commands/definitions'; +import { + topLevelCommands, + appCommandGroup, + skillCommandGroup, + functionCommandGroup, +} from '../commands/definitions'; import { formatBlockedBanner, startUpdateCheck, @@ -105,7 +110,7 @@ client.setEnsureFresh(async () => { // ──────────────── Register all commands ──────────────── -registerAll(program, topLevelCommands, [appCommandGroup, skillCommandGroup]); +registerAll(program, topLevelCommands, [appCommandGroup, skillCommandGroup, functionCommandGroup]); // ──────────────── Re-auth handler ──────────────── diff --git a/src/commands/app/create.ts b/src/commands/app/create.ts index e57b9c1..fdd2027 100644 --- a/src/commands/app/create.ts +++ b/src/commands/app/create.ts @@ -110,9 +110,9 @@ async function resolveAppName(nameFlag: string | undefined): Promise { // to a shape that can still change. Any non-interactive run — piped stdin or // `--json` — creates an OAuth app, exactly as it did before BEX-290, so // existing scripted `app create` calls are unaffected. -export type AppType = 'oauth' | 'ui'; +export type AppType = 'oauth' | 'ui' | 'function'; -async function resolveAppType(interactive: boolean): Promise { +async function resolveAppType(interactive: boolean, distribution?: string): Promise { // A UI app is only reachable through this prompt (there is no `--type` flag), so a // non-interactive run has nothing to resolve and creates an OAuth app, exactly as it // did before BEX-290. @@ -136,6 +136,11 @@ async function resolveAppType(interactive: boolean): Promise { if (__BREVO_PREVIEW__ && isFeatureAvailable('ui-app-type')) { choices.push({ name: messages.APP_CREATE_APP_TYPE_UI, value: 'ui' }); } + // Brevo Function is offered only for private apps — not behind __BREVO_PREVIEW__, + // since it ships in the published build. + if (distribution === 'private') { + choices.push({ name: messages.APP_CREATE_APP_TYPE_FUNCTION, value: 'function' }); + } const answer = await inquirer.prompt([ { type: 'list', @@ -367,6 +372,8 @@ interface CreateAppInputs { logoUri?: string; /** Present for UI apps only; drives scope defaults and omits redirect URIs. */ uiApp?: UiApp; + /** The selected app type — drives the `brevo_function` discriminator on the wire. */ + appType: AppType; } interface CreatedApp { @@ -396,6 +403,9 @@ function buildCreatePayload(inputs: CreateAppInputs) { ...(isUiApp ? { ui_app: inputs.uiApp } : { auth: { scopes: [...DEFAULT_SCOPES], redirect_uris: inputs.redirectUris } }), + // Brevo Function apps are created as OAuth apps on the wire — the server + // does not accept a `brevo_function` block on create. The discriminator + // lives in the local app-config.json snapshot instead (see the template). ...(inputs.logoUri ? { logo_uri: inputs.logoUri } : {}), }; } @@ -574,10 +584,10 @@ export const createCommand = withCommandHandler( const appName = await resolveAppName(options.name); const logoUri = await resolveLogoUri(options.logoUri, jsonMode); const distribution = await resolveDistribution(options.distribution, interactive); - const appType = await resolveAppType(interactive); + const appType = await resolveAppType(interactive, distribution); - // The two app types diverge here: OAuth apps collect callback URLs, UI apps - // collect placement + destination. Neither path runs the other's prompts. + // The app types diverge here: OAuth and Function apps collect callback URLs, + // UI apps collect placement + destination. Neither path runs the other's prompts. let redirectUris: string[] = []; let uiApp: UiApp | undefined; // Same elimination site as `resolveAppType`: this is the only call to @@ -588,12 +598,20 @@ export const createCommand = withCommandHandler( if (__BREVO_PREVIEW__ && appType === 'ui') { uiApp = await resolveUiApp(); } else { + // Both OAuth and Function apps follow the OAuth path (collect redirect URIs). redirectUris = await resolveRedirectUrls(options.redirectUri, jsonMode); } const dir = await resolveCreateDirectory(appName, interactive); - const inputs: CreateAppInputs = { appName, distribution, redirectUris, logoUri, uiApp }; + const inputs: CreateAppInputs = { + appName, + distribution, + redirectUris, + logoUri, + uiApp, + appType, + }; const { result, appName: finalAppName } = await createAppWithRetry( inputs, jsonMode, @@ -692,6 +710,9 @@ export const createCommand = withCommandHandler( redirect_uris: result.redirect_uris ?? null, }; const ctx = await fetchAppContext(result.app_id, jsonMode, uiApp, fallbackApp); + if (appType === 'function') { + ctx.isBrevoFunction = true; + } // Always write the basic project structure (app-config.json + meta files). const base = runBaseScaffold(result.app_id, ctx, dir.targetDir, dir.mergeOnly); diff --git a/src/commands/app/project-writer.ts b/src/commands/app/project-writer.ts index d916e57..2682d5d 100644 --- a/src/commands/app/project-writer.ts +++ b/src/commands/app/project-writer.ts @@ -103,6 +103,8 @@ export interface AppContext { * Absent for OAuth apps. */ uiApp?: UiApp; + /** True when the app is a Brevo Function. Drives `brevo_function: {}` in app-config.json. */ + isBrevoFunction?: boolean; } export function computeSlug(name: string | undefined): string { @@ -456,6 +458,9 @@ function buildTemplateVars(appId: string, ctx: AppContext, targetDir: string): T // Empty for OAuth apps — its emptiness is what selects the `oauth` // conditional branch in templates (see resolveTemplateFlags). '{{UI_APP_JSON}}': renderUiAppJson(ctx.uiApp), + // Non-empty for Brevo Function apps — rendered as `"brevo_function": {}` + // in app-config.json so the app type is preserved in the local snapshot. + '{{BREVO_FUNCTION_JSON}}': ctx.isBrevoFunction ? '{}' : '', }; return { vars, scopes, legacyAllSubstituted }; diff --git a/src/commands/definitions.ts b/src/commands/definitions.ts index f1364ea..a184b96 100644 --- a/src/commands/definitions.ts +++ b/src/commands/definitions.ts @@ -22,6 +22,8 @@ import { scopesCommand } from './app/scopes'; import { startCommand } from './app/start'; import { installCommand as skillInstallCommand } from './skill/install'; import { uninstallCommand as skillUninstallCommand } from './skill/uninstall'; +import { listFunctionCommand } from './function/list'; +import { getFunctionCommand } from './function/get'; export const topLevelCommands: CommandDefinition[] = [ { @@ -280,3 +282,33 @@ export const skillCommandGroup: SubcommandGroupDefinition = { }, ], }; + +export const functionCommandGroup: SubcommandGroupDefinition = { + name: 'function', + description: 'Manage Brevo Functions', + commands: [ + { + name: 'list', + description: 'List all Brevo Functions in your account', + examples: [ + 'brevo function list', + 'brevo function list --draft', + 'brevo function list --json', + ], + options: [ + { flags: '--draft', description: 'List only draft functions' }, + { flags: '--json', description: 'Output as JSON' }, + ], + handler: (opts) => + listFunctionCommand({ json: Boolean(opts.json), draft: Boolean(opts.draft) }), + }, + { + name: 'get', + description: 'Show details of a Brevo Function', + arguments: [{ name: '', description: 'Function ID' }], + examples: ['brevo function get fn-001', 'brevo function get fn-001 --json'], + options: [{ flags: '--json', description: 'Output as JSON' }], + handler: (opts, id) => getFunctionCommand({ id: id as string, json: Boolean(opts.json) }), + }, + ], +}; diff --git a/src/commands/function/get.ts b/src/commands/function/get.ts new file mode 100644 index 0000000..13032cb --- /dev/null +++ b/src/commands/function/get.ts @@ -0,0 +1,54 @@ +import { logInfo, logWarn } from '../../lib/logger'; +import { messages } from '../../lang/en'; +import { functionService } from '../../container'; +import { withCommandHandler } from '../../lib/command-handler'; +import { jsonOutput } from '../../lib/json-output'; +import { createSpinner } from '../../lib/ui'; +import { ApiError } from '../../lib/errors'; + +export const getFunctionCommand = withCommandHandler( + async (options: { id: string; json?: boolean }): Promise => { + const spinner = createSpinner('Fetching Brevo Function...', { silent: options.json }); + let fn; + try { + fn = await functionService.fetchFunction(options.id); + } catch (err) { + if (err instanceof ApiError && err.statusCode === 404) { + spinner.stop(); + if (options.json) { + jsonOutput({ error: 'not_found', message: messages.FUNCTION_GET_NOT_FOUND(options.id) }); + return; + } + logWarn(`\n ${messages.FUNCTION_GET_NOT_FOUND(options.id)}\n`); + return; + } + throw err; + } finally { + spinner.stop(); + } + + if (options.json) { + jsonOutput(fn); + return; + } + + logInfo(`\n ${messages.FUNCTION_GET_HEADER}\n`); + process.stdout.write(` Name: ${fn.name}\n`); + process.stdout.write(` ID: ${fn.id}\n`); + process.stdout.write(` Status: ${fn.is_active ? 'active' : 'inactive'}\n`); + process.stdout.write(` Description: ${fn.description}\n`); + process.stdout.write(` Explanation: ${fn.explanation}\n`); + process.stdout.write(` Formula: ${fn.formula}\n`); + if (fn.category) { + process.stdout.write(` Category: ${fn.category}\n`); + } + process.stdout.write(` Version: ${fn.version}\n`); + process.stdout.write(` Global: ${fn.is_global ? 'yes' : 'no'}\n`); + process.stdout.write(` Created: ${fn.created_at}\n`); + process.stdout.write(` Updated: ${fn.updated_at}\n`); + if (fn.last_recalculated_at) { + process.stdout.write(` Recalculated: ${fn.last_recalculated_at}\n`); + } + process.stdout.write('\n'); + }, +); diff --git a/src/commands/function/list.ts b/src/commands/function/list.ts new file mode 100644 index 0000000..eaf17a1 --- /dev/null +++ b/src/commands/function/list.ts @@ -0,0 +1,84 @@ +import { logInfo } from '../../lib/logger'; +import { messages } from '../../lang/en'; +import { functionService } from '../../container'; +import { withCommandHandler } from '../../lib/command-handler'; +import { jsonOutput } from '../../lib/json-output'; +import { createSpinner } from '../../lib/ui'; + +export const listFunctionCommand = withCommandHandler( + async (options: { json?: boolean; draft?: boolean }): Promise => { + if (options.draft) { + return listDraftFunctions(options); + } + return listPublishedFunctions(options); + }, +); + +async function listPublishedFunctions(options: { json?: boolean }): Promise { + const spinner = createSpinner('Fetching Brevo Functions...', { silent: options.json }); + let response; + try { + response = await functionService.fetchFunctionList(); + } finally { + spinner.stop(); + } + + const functions = response.functions ?? []; + + if (options.json) { + jsonOutput(response); + return; + } + + if (functions.length === 0) { + logInfo(`\n ${messages.FUNCTION_LIST_EMPTY}\n`); + return; + } + + logInfo(`\n ${messages.FUNCTION_LIST_HEADER}\n`); + + for (const fn of functions) { + const status = fn.is_active ? 'active' : 'inactive'; + process.stdout.write(` ${fn.name} (ID: ${fn.id})\n`); + process.stdout.write(` Status: ${status}\n`); + process.stdout.write(` Formula: ${fn.formula}\n`); + process.stdout.write('\n'); + } + + process.stdout.write(` Total: ${response.total} / ${response.max}\n\n`); +} + +async function listDraftFunctions(options: { json?: boolean }): Promise { + const spinner = createSpinner('Fetching draft Brevo Functions...', { silent: options.json }); + let response; + try { + response = await functionService.fetchDraftFunctionList(); + } finally { + spinner.stop(); + } + + const drafts = response.drafts ?? []; + + if (options.json) { + jsonOutput(response); + return; + } + + if (drafts.length === 0) { + logInfo(`\n ${messages.FUNCTION_LIST_DRAFT_EMPTY}\n`); + return; + } + + logInfo(`\n ${messages.FUNCTION_LIST_DRAFT_HEADER}\n`); + + for (const fn of drafts) { + process.stdout.write(` ${fn.id}\n`); + process.stdout.write(` Description: ${fn.description}\n`); + process.stdout.write(` Formula: ${fn.formula}\n`); + process.stdout.write(` Created: ${fn.created_at}\n`); + process.stdout.write(` Expires: ${fn.expires_at}\n`); + process.stdout.write('\n'); + } + + process.stdout.write(` Total: ${response.total}\n\n`); +} diff --git a/src/container.ts b/src/container.ts index 8ca07aa..34741c5 100644 --- a/src/container.ts +++ b/src/container.ts @@ -1,6 +1,7 @@ import { ApiClient } from './api/client'; import { createAccountService, AccountService } from './services/account'; import { createAppService, AppService } from './services/app'; +import { createFunctionService, FunctionService } from './services/function'; import { API_BASE } from './lib/constants'; import { getAuthCred } from './lib/config'; @@ -24,3 +25,4 @@ export const client = new ApiClient({ baseUrl: API_BASE, getAuthHeader: buildAut export const accountService: AccountService = createAccountService(client); export const appService: AppService = createAppService(client); +export const functionService: FunctionService = createFunctionService(client); diff --git a/src/lang/en.ts b/src/lang/en.ts index 8b4977b..074ab0c 100644 --- a/src/lang/en.ts +++ b/src/lang/en.ts @@ -92,8 +92,11 @@ const coreMessages = { APP_CREATE_TYPE_PROMPT: 'Distribution type?', APP_CREATE_APP_TYPE_PROMPT: 'What type of app are you building?', APP_CREATE_APP_TYPE_OAUTH: - 'OAuth app (Authorize against Brevo and call the API on a user’s behalf)', - APP_CREATE_APP_TYPE_UI: 'UI app (Render inside Brevo — opens your app from a record)', + 'OAuth app (Authorize against Brevo and call the API on a user’s behalf)', + APP_CREATE_APP_TYPE_UI: + 'UI app (Render inside Brevo \u2014 opens your app from a record)', + APP_CREATE_APP_TYPE_FUNCTION: + 'Brevo Function (Serverless function running on Brevo’s infrastructure)', APP_CREATE_SUCCESS: 'App created.', APP_CREATE_NAME_TAKEN: 'That name is already taken. Try a different name.', // Shown only after every prompt has been answered — hence the reassurance: @@ -186,6 +189,16 @@ const coreMessages = { APP_SELECT_NON_INTERACTIVE: (command: string) => `Cannot show the app picker in non-interactive mode. Name the app instead:\n\n ${command}\n\n \`${CLI.APP_LIST}\` shows the IDs.`, + // Function list + FUNCTION_LIST_HEADER: 'Your Brevo Functions:', + FUNCTION_LIST_EMPTY: 'No Brevo Functions found. You have not created any Brevo Functions yet.', + FUNCTION_LIST_DRAFT_HEADER: 'Your draft Brevo Functions:', + FUNCTION_LIST_DRAFT_EMPTY: 'No draft Brevo Functions found.', + + // Function get + FUNCTION_GET_HEADER: 'Brevo Function details:', + FUNCTION_GET_NOT_FOUND: (id: string) => `Brevo Function "${id}" not found.`, + // App credentials APP_CREDENTIALS_REVEAL_CONFIRM: 'Are you sure you want to reveal the client secret?', APP_CREDENTIALS_SELECT: 'Select an app:', diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 2b8aa32..1293d3c 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -148,6 +148,8 @@ export const ENDPOINTS = { // record-page prompt and then narrows the row read with `?location=`, rather than // pulling the whole registry to derive the same handful of strings client-side. APP_STORE_SURFACE_POINT_LOCATIONS: '/v3/app-store/surface-points/locations', + DP_FUNCTIONS: '/v3/dp-functions/functions', + DP_FUNCTION: (id: string) => `/v3/dp-functions/functions/${encodeURIComponent(id)}`, OAUTH_AUTHORIZE: '/oauth/authorize', OAUTH_TOKEN: '/oauth/token', } as const; @@ -188,6 +190,8 @@ export const CLI = { APP_START: (feature?: string) => feature ? `brevo app start ${feature}` : 'brevo app start ', APP_SCOPES: 'brevo app available-scopes', + FUNCTION_LIST: 'brevo function list', + FUNCTION_GET: 'brevo function get', SKILL_INSTALL: 'brevo skill:cli install', SKILL_UNINSTALL: 'brevo skill:cli uninstall', } as const; diff --git a/src/lib/help.ts b/src/lib/help.ts index 3ed1971..c9a0657 100644 --- a/src/lib/help.ts +++ b/src/lib/help.ts @@ -114,6 +114,10 @@ function formatRootHelp(description: string): string { ` brevo skill:cli install [--json] Install the brevo-cli Claude Code skill`, ` brevo skill:cli uninstall [--json] Remove the brevo-cli skill`, ``, + `Function commands:`, + ` brevo function list [--draft] [--json] List all Brevo Functions in your account`, + ` brevo function get [--json] Show details of a Brevo Function`, + ``, `Scope commands:`, ` brevo app available-scopes [--web] [--json] List OAuth scopes supported by the IdP`, ` (--web opens the catalog in a browser)`, diff --git a/src/services/function.ts b/src/services/function.ts new file mode 100644 index 0000000..19cda7f --- /dev/null +++ b/src/services/function.ts @@ -0,0 +1,23 @@ +import { ApiClient } from '../api/client'; +import { ENDPOINTS } from '../lib/constants'; +import { DpFunction, DpFunctionListResponse, DpDraftFunctionListResponse } from '../types'; + +export function createFunctionService(client: ApiClient) { + return { + async fetchFunctionList(): Promise { + const params = new URLSearchParams({ limit: '50', offset: '0' }); + return client.get(`${ENDPOINTS.DP_FUNCTIONS}?${params}`); + }, + + async fetchDraftFunctionList(): Promise { + const params = new URLSearchParams({ limit: '50', offset: '0', draft: 'true' }); + return client.get(`${ENDPOINTS.DP_FUNCTIONS}?${params}`); + }, + + async fetchFunction(id: string): Promise { + return client.get(ENDPOINTS.DP_FUNCTION(id)); + }, + }; +} + +export type FunctionService = ReturnType; diff --git a/src/templates/index.ts b/src/templates/index.ts index 47d612f..d1a2061 100644 --- a/src/templates/index.ts +++ b/src/templates/index.ts @@ -32,10 +32,12 @@ export type Distribution = 'public' | 'private'; * OAuth flow). * - `oauth` / `ui_app` — the app *type* (BEX-290). Exactly one is always set, * so a template can carry OAuth-only and UI-app-only sections side by side. + * - `brevo_function` — set when the app is a Brevo Function. Orthogonal to + * `oauth` (a Function app is also an OAuth app on the wire). */ -export type TemplateFlag = 'public' | 'private' | 'oauth' | 'ui_app'; +export type TemplateFlag = 'public' | 'private' | 'oauth' | 'ui_app' | 'brevo_function'; -const IF_OPEN_RE = /^\s*\{\{#if (public|private|oauth|ui_app)\}\}\s*$/; +const IF_OPEN_RE = /^\s*\{\{#if (public|private|oauth|ui_app|brevo_function)\}\}\s*$/; const IF_CLOSE_RE = /^\s*\{\{\/if\}\}\s*$/; /** @@ -174,7 +176,10 @@ export const FEATURE_LABELS: Record = { function resolveTemplateFlags(vars: Record): Set { const distribution: Distribution = vars['{{DISTRIBUTION}}'] === 'public' ? 'public' : 'private'; const isUiApp = !!vars['{{UI_APP_JSON}}']; - return new Set([distribution, isUiApp ? 'ui_app' : 'oauth']); + const isBrevoFunction = !!vars['{{BREVO_FUNCTION_JSON}}']; + const flags = new Set([distribution, isUiApp ? 'ui_app' : 'oauth']); + if (isBrevoFunction) flags.add('brevo_function'); + return flags; } function loadManifest( diff --git a/src/types.ts b/src/types.ts index 77dad98..e08c802 100644 --- a/src/types.ts +++ b/src/types.ts @@ -478,3 +478,48 @@ export interface CliInfoQuery { cliVersion: string; reason: string; } + +// ──────────────── Brevo Functions ──────────────── + +export interface DpFunction { + id: string; + name: string; + description: string; + explanation: string; + formula: string; + category?: string; + attribute_id?: string; + attribute_type?: string; + version: number; + is_active: boolean; + is_global: boolean; + created_at: string; + updated_at: string; + last_recalculated_at?: string; +} + +export interface DpFunctionListResponse { + functions: DpFunction[]; + total: number; + max: number; + limit: number; + offset: number; + has_more: boolean; +} + +export interface DpDraftFunction { + id: string; + description: string; + explanation: string; + formula: string; + created_at: string; + expires_at: string; +} + +export interface DpDraftFunctionListResponse { + drafts: DpDraftFunction[]; + total: number; + limit: number; + offset: number; + has_more: boolean; +} diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index 5895bb0..bf102bf 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -2,7 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "rootDir": ".", - "noEmit": true + "noEmit": true, + "types": ["jest", "node"] }, "include": ["src/**/*", "scripts/**/*"], "exclude": ["node_modules", "dist"] diff --git a/yarn.lock b/yarn.lock index e6e1f29..636ab51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.28.6": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0": version "7.29.0" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz" integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw== @@ -11,34 +11,25 @@ js-tokens "^4.0.0" picocolors "^1.1.1" -"@babel/code-frame@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.7.tgz#f2fbbfea87c44a21590ec515b778b2c26d8866e7" - integrity sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw== - dependencies: - "@babel/helper-validator-identifier" "^7.29.7" - js-tokens "^4.0.0" - picocolors "^1.1.1" +"@babel/compat-data@^7.28.6": + version "7.29.0" + resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz" + integrity sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg== -"@babel/compat-data@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.7.tgz#6f0237f0f36d2e51c0570a636faed9d2d0efe629" - integrity sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg== - -"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.9": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.7.tgz#80c10b17248082968b57a857b91640971f2070f7" - integrity sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA== - dependencies: - "@babel/code-frame" "^7.29.7" - "@babel/generator" "^7.29.7" - "@babel/helper-compilation-targets" "^7.29.7" - "@babel/helper-module-transforms" "^7.29.7" - "@babel/helpers" "^7.29.7" - "@babel/parser" "^7.29.7" - "@babel/template" "^7.29.7" - "@babel/traverse" "^7.29.7" - "@babel/types" "^7.29.7" +"@babel/core@^7.0.0", "@babel/core@^7.0.0 || ^8.0.0-0", "@babel/core@^7.0.0-0", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.9", "@babel/core@^7.8.0", "@babel/core@>=7.0.0-beta.0 <8": + version "7.29.0" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz" + integrity sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA== + dependencies: + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-module-transforms" "^7.28.6" + "@babel/helpers" "^7.28.6" + "@babel/parser" "^7.29.0" + "@babel/template" "^7.28.6" + "@babel/traverse" "^7.29.0" + "@babel/types" "^7.29.0" "@jridgewell/remapping" "^2.3.5" convert-source-map "^2.0.0" debug "^4.1.0" @@ -46,18 +37,7 @@ json5 "^2.2.3" semver "^6.3.1" -"@babel/generator@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.7.tgz#cca0b8827e6bcf3ba176788e7f3b180ad6db2fa3" - integrity sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ== - dependencies: - "@babel/parser" "^7.29.7" - "@babel/types" "^7.29.7" - "@jridgewell/gen-mapping" "^0.3.12" - "@jridgewell/trace-mapping" "^0.3.28" - jsesc "^3.0.2" - -"@babel/generator@^7.7.2": +"@babel/generator@^7.29.0", "@babel/generator@^7.7.2": version "7.29.1" resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz" integrity sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw== @@ -68,38 +48,38 @@ "@jridgewell/trace-mapping" "^0.3.28" jsesc "^3.0.2" -"@babel/helper-compilation-targets@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz#7a1def704302401c47f64fa85589e974ae217042" - integrity sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g== +"@babel/helper-compilation-targets@^7.28.6": + version "7.28.6" + resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz" + integrity sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA== dependencies: - "@babel/compat-data" "^7.29.7" - "@babel/helper-validator-option" "^7.29.7" + "@babel/compat-data" "^7.28.6" + "@babel/helper-validator-option" "^7.27.1" browserslist "^4.24.0" lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-globals@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz#f04a96fbd8473241b1079243f5b3f03a3010ab7b" - integrity sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA== +"@babel/helper-globals@^7.28.0": + version "7.28.0" + resolved "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz" + integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== -"@babel/helper-module-imports@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz#ef25048a518e828d7393fac5882ddd73921d7396" - integrity sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g== +"@babel/helper-module-imports@^7.28.6": + version "7.28.6" + resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz" + integrity sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw== dependencies: - "@babel/traverse" "^7.29.7" - "@babel/types" "^7.29.7" + "@babel/traverse" "^7.28.6" + "@babel/types" "^7.28.6" -"@babel/helper-module-transforms@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz#b062747a5997ba138637201328bbff77960574ae" - integrity sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg== +"@babel/helper-module-transforms@^7.28.6": + version "7.28.6" + resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz" + integrity sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA== dependencies: - "@babel/helper-module-imports" "^7.29.7" - "@babel/helper-validator-identifier" "^7.29.7" - "@babel/traverse" "^7.29.7" + "@babel/helper-module-imports" "^7.28.6" + "@babel/helper-validator-identifier" "^7.28.5" + "@babel/traverse" "^7.28.6" "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.28.6", "@babel/helper-plugin-utils@^7.8.0": version "7.28.6" @@ -111,33 +91,23 @@ resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz" integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== -"@babel/helper-string-parser@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz#7f0871d99824d23137d60f86fcf6130fd5a1b51f" - integrity sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw== - "@babel/helper-validator-identifier@^7.28.5": version "7.28.5" resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz" integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== -"@babel/helper-validator-identifier@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz#bd87084ced0c796ec46bda492de6e83d29e89fc2" - integrity sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg== - -"@babel/helper-validator-option@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz#cf315be940213b354eb4abcc0bd01ebe3f73bc2a" - integrity sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw== +"@babel/helper-validator-option@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz" + integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== -"@babel/helpers@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.29.7.tgz#45abfde7548997e34376c3e69feb475cffb4a607" - integrity sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg== +"@babel/helpers@^7.28.6": + version "7.29.2" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz" + integrity sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw== dependencies: - "@babel/template" "^7.29.7" - "@babel/types" "^7.29.7" + "@babel/template" "^7.28.6" + "@babel/types" "^7.29.0" "@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.28.6", "@babel/parser@^7.29.0": version "7.29.2" @@ -146,13 +116,6 @@ dependencies: "@babel/types" "^7.29.0" -"@babel/parser@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.7.tgz#837b87387cbf5ec5530cb634b3c622f68edb9334" - integrity sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg== - dependencies: - "@babel/types" "^7.29.7" - "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz" @@ -274,19 +237,10 @@ "@babel/runtime@^7.5.5": version "7.29.2" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.2.tgz#9a6e2d05f4b6692e1801cd4fb176ad823930ed5e" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz" integrity sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g== -"@babel/template@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.29.7.tgz#4d9d4004f645cdd304de958c725162784ecac700" - integrity sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg== - dependencies: - "@babel/code-frame" "^7.29.7" - "@babel/parser" "^7.29.7" - "@babel/types" "^7.29.7" - -"@babel/template@^7.3.3": +"@babel/template@^7.28.6", "@babel/template@^7.3.3": version "7.28.6" resolved "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz" integrity sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ== @@ -295,17 +249,17 @@ "@babel/parser" "^7.28.6" "@babel/types" "^7.28.6" -"@babel/traverse@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.7.tgz#c47b07a41b95da0907d026b5dd894d98de7d2f2d" - integrity sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw== - dependencies: - "@babel/code-frame" "^7.29.7" - "@babel/generator" "^7.29.7" - "@babel/helper-globals" "^7.29.7" - "@babel/parser" "^7.29.7" - "@babel/template" "^7.29.7" - "@babel/types" "^7.29.7" +"@babel/traverse@^7.28.6", "@babel/traverse@^7.29.0": + version "7.29.0" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz" + integrity sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA== + dependencies: + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.29.0" + "@babel/template" "^7.28.6" + "@babel/types" "^7.29.0" debug "^4.3.1" "@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.28.2", "@babel/types@^7.28.6", "@babel/types@^7.29.0", "@babel/types@^7.3.3": @@ -316,14 +270,6 @@ "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.28.5" -"@babel/types@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.7.tgz#8005e31d82712ee7adaef6e23c63b71a62770a92" - integrity sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA== - dependencies: - "@babel/helper-string-parser" "^7.29.7" - "@babel/helper-validator-identifier" "^7.29.7" - "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" @@ -331,7 +277,7 @@ "@changesets/apply-release-plan@^7.1.0": version "7.1.0" - resolved "https://registry.yarnpkg.com/@changesets/apply-release-plan/-/apply-release-plan-7.1.0.tgz#2bed6c4b755b1836810b564c243ea9e8eb411c4b" + resolved "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.1.0.tgz" integrity sha512-yq8ML3YS7koKQ/9bk1PqO0HMzApIFNwjlwCnwFEXMzNe8NpzeeYYKCmnhWJGkN8g7E51MnWaSbqRcTcdIxUgnQ== dependencies: "@changesets/config" "^3.1.3" @@ -350,7 +296,7 @@ "@changesets/assemble-release-plan@^6.0.9": version "6.0.9" - resolved "https://registry.yarnpkg.com/@changesets/assemble-release-plan/-/assemble-release-plan-6.0.9.tgz#8aa5baf0037a85812e320172e83b92ca31e85fd6" + resolved "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-6.0.9.tgz" integrity sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ== dependencies: "@changesets/errors" "^0.2.0" @@ -362,14 +308,14 @@ "@changesets/changelog-git@^0.2.1": version "0.2.1" - resolved "https://registry.yarnpkg.com/@changesets/changelog-git/-/changelog-git-0.2.1.tgz#7f311f3dc11eae1235aa7fd2c1807112962b409b" + resolved "https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-0.2.1.tgz" integrity sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q== dependencies: "@changesets/types" "^6.1.0" "@changesets/cli@^2.30.0": version "2.30.0" - resolved "https://registry.yarnpkg.com/@changesets/cli/-/cli-2.30.0.tgz#b723db4036705b047257dfd05aeef0c9a544497f" + resolved "https://registry.npmjs.org/@changesets/cli/-/cli-2.30.0.tgz" integrity sha512-5D3Nk2JPqMI1wK25pEymeWRSlSMdo5QOGlyfrKg0AOufrUcjEE3RQgaCpHoBiM31CSNrtSgdJ0U6zL1rLDDfBA== dependencies: "@changesets/apply-release-plan" "^7.1.0" @@ -401,7 +347,7 @@ "@changesets/config@^3.1.3": version "3.1.3" - resolved "https://registry.yarnpkg.com/@changesets/config/-/config-3.1.3.tgz#e261712c6912ec40d7b7d490a1c2b7f2464f2e41" + resolved "https://registry.npmjs.org/@changesets/config/-/config-3.1.3.tgz" integrity sha512-vnXjcey8YgBn2L1OPWd3ORs0bGC4LoYcK/ubpgvzNVr53JXV5GiTVj7fWdMRsoKUH7hhhMAQnsJUqLr21EncNw== dependencies: "@changesets/errors" "^0.2.0" @@ -415,14 +361,14 @@ "@changesets/errors@^0.2.0": version "0.2.0" - resolved "https://registry.yarnpkg.com/@changesets/errors/-/errors-0.2.0.tgz#3c545e802b0f053389cadcf0ed54e5636ff9026a" + resolved "https://registry.npmjs.org/@changesets/errors/-/errors-0.2.0.tgz" integrity sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow== dependencies: extendable-error "^0.1.5" "@changesets/get-dependents-graph@^2.1.3": version "2.1.3" - resolved "https://registry.yarnpkg.com/@changesets/get-dependents-graph/-/get-dependents-graph-2.1.3.tgz#cd31b39daab7102921fb65acdcb51b4658502eee" + resolved "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-2.1.3.tgz" integrity sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ== dependencies: "@changesets/types" "^6.1.0" @@ -432,7 +378,7 @@ "@changesets/get-release-plan@^4.0.15": version "4.0.15" - resolved "https://registry.yarnpkg.com/@changesets/get-release-plan/-/get-release-plan-4.0.15.tgz#7c9280de38682a8bf1b4c5ea2639198670fa0ca9" + resolved "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.15.tgz" integrity sha512-Q04ZaRPuEVZtA+auOYgFaVQQSA98dXiVe/yFaZfY7hoSmQICHGvP0TF4u3EDNHWmmCS4ekA/XSpKlSM2PyTS2g== dependencies: "@changesets/assemble-release-plan" "^6.0.9" @@ -444,12 +390,12 @@ "@changesets/get-version-range-type@^0.4.0": version "0.4.0" - resolved "https://registry.yarnpkg.com/@changesets/get-version-range-type/-/get-version-range-type-0.4.0.tgz#429a90410eefef4368502c41c63413e291740bf5" + resolved "https://registry.npmjs.org/@changesets/get-version-range-type/-/get-version-range-type-0.4.0.tgz" integrity sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ== "@changesets/git@^3.0.4": version "3.0.4" - resolved "https://registry.yarnpkg.com/@changesets/git/-/git-3.0.4.tgz#75e3811ab407ec010beb51131ceb5c6b3975c914" + resolved "https://registry.npmjs.org/@changesets/git/-/git-3.0.4.tgz" integrity sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw== dependencies: "@changesets/errors" "^0.2.0" @@ -460,14 +406,14 @@ "@changesets/logger@^0.1.1": version "0.1.1" - resolved "https://registry.yarnpkg.com/@changesets/logger/-/logger-0.1.1.tgz#9926ac4dc8fb00472fe1711603b6b4755d64b435" + resolved "https://registry.npmjs.org/@changesets/logger/-/logger-0.1.1.tgz" integrity sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg== dependencies: picocolors "^1.1.0" "@changesets/parse@^0.4.3": version "0.4.3" - resolved "https://registry.yarnpkg.com/@changesets/parse/-/parse-0.4.3.tgz#912a7eac7f8cb387b05f749596a68f2f763660e0" + resolved "https://registry.npmjs.org/@changesets/parse/-/parse-0.4.3.tgz" integrity sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A== dependencies: "@changesets/types" "^6.1.0" @@ -475,7 +421,7 @@ "@changesets/pre@^2.0.2": version "2.0.2" - resolved "https://registry.yarnpkg.com/@changesets/pre/-/pre-2.0.2.tgz#b35e84d25fca8b970340642ca04ce76c7fc34ced" + resolved "https://registry.npmjs.org/@changesets/pre/-/pre-2.0.2.tgz" integrity sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug== dependencies: "@changesets/errors" "^0.2.0" @@ -485,7 +431,7 @@ "@changesets/read@^0.6.7": version "0.6.7" - resolved "https://registry.yarnpkg.com/@changesets/read/-/read-0.6.7.tgz#65e144c960344b34ae8bd85144752a4b687e92d2" + resolved "https://registry.npmjs.org/@changesets/read/-/read-0.6.7.tgz" integrity sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA== dependencies: "@changesets/git" "^3.0.4" @@ -498,7 +444,7 @@ "@changesets/should-skip-package@^0.1.2": version "0.1.2" - resolved "https://registry.yarnpkg.com/@changesets/should-skip-package/-/should-skip-package-0.1.2.tgz#c018e1e05eab3d97afa4c4590f2b0db7486ae488" + resolved "https://registry.npmjs.org/@changesets/should-skip-package/-/should-skip-package-0.1.2.tgz" integrity sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw== dependencies: "@changesets/types" "^6.1.0" @@ -506,17 +452,17 @@ "@changesets/types@^4.0.1": version "4.1.0" - resolved "https://registry.yarnpkg.com/@changesets/types/-/types-4.1.0.tgz#fb8f7ca2324fd54954824e864f9a61a82cb78fe0" + resolved "https://registry.npmjs.org/@changesets/types/-/types-4.1.0.tgz" integrity sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw== "@changesets/types@^6.1.0": version "6.1.0" - resolved "https://registry.yarnpkg.com/@changesets/types/-/types-6.1.0.tgz#12a4c8490827d26bc6fbf97a151499be2fb6d2f5" + resolved "https://registry.npmjs.org/@changesets/types/-/types-6.1.0.tgz" integrity sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA== "@changesets/write@^0.4.0": version "0.4.0" - resolved "https://registry.yarnpkg.com/@changesets/write/-/write-0.4.0.tgz#ec903cbd8aa9b6da6fa09ef19fb609eedd115ed6" + resolved "https://registry.npmjs.org/@changesets/write/-/write-0.4.0.tgz" integrity sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q== dependencies: "@changesets/types" "^6.1.0" @@ -524,135 +470,15 @@ human-id "^4.1.1" prettier "^2.7.1" -"@esbuild/aix-ppc64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz#7a01a8d2ec2fbb2dac78adad09b0fa781e4082be" - integrity sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ== - -"@esbuild/android-arm64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz#b540a27d14e4afd058496a4dbec4d3f414db110a" - integrity sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg== - -"@esbuild/android-arm@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.28.1.tgz#704bd297de6d762de54eabbeafbf55f6756abe2f" - integrity sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ== - -"@esbuild/android-x64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.28.1.tgz#d1cb166d34b0fbf0fe8ab460a5594f24a378701e" - integrity sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng== - -"@esbuild/darwin-arm64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz#1034b26457fc886368fe61bbd09f653f6afa8e54" - integrity sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q== - -"@esbuild/darwin-x64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz#65556a432a1e4d72032d8218c1932fcca1a49772" - integrity sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ== - -"@esbuild/freebsd-arm64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz#2e61e0592f9030d7e3dae18ee25ebc535918aef6" - integrity sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw== - -"@esbuild/freebsd-x64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz#c95ec289959ef8079c4dca817a1e2c4be66b9bd3" - integrity sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ== - -"@esbuild/linux-arm64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz#40b22175dda06182f3ee8141186c5ff304c4a717" - integrity sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g== - -"@esbuild/linux-arm@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz#c09a0f67917592ac0de892a9be4d3814debd2a6c" - integrity sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ== - -"@esbuild/linux-ia32@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz#a580f9c676797833891e519fc7a1337c8afd8db3" - integrity sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w== - -"@esbuild/linux-loong64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz#46452cf321dc7f9e91c2fa780a56bb56e79cd68b" - integrity sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg== - -"@esbuild/linux-mips64el@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz#4211b3184dd6608f53dcb22e39f5d34ee08852c8" - integrity sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ== - -"@esbuild/linux-ppc64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz#697857c2a61cb9b0b6bb6652e40c1dc5e1ca8e5d" - integrity sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ== - -"@esbuild/linux-riscv64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz#d192943eb146a40ac4c6497d0cf7be35b986bf08" - integrity sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ== - -"@esbuild/linux-s390x@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz#acea0356da0e0ebc08f97cf7b9c2e401e1e648dc" - integrity sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag== - -"@esbuild/linux-x64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz#6f0c3ce0cb64c534b70c4c45ecb2c16d34e35dfd" - integrity sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA== - -"@esbuild/netbsd-arm64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz#8bcd77077a0dce3378b574fedb26d2a253b73d36" - integrity sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw== - -"@esbuild/netbsd-x64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz#e7fb2a01e99c830c94e6623cd9fefb4c8fb58347" - integrity sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg== - -"@esbuild/openbsd-arm64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz#c52909372db8b86e2c55e05a8940033b5660a3b2" - integrity sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q== - -"@esbuild/openbsd-x64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz#c427b9be5a64c262ff9a7eb70b5fbbaadf446c6c" - integrity sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw== - -"@esbuild/openharmony-arm64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz#dc9b147baca2e6c4b3c85571741ef4860a489097" - integrity sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg== - -"@esbuild/sunos-x64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz#ce866d12df13c15e4c99f073a3d466f6e0649b3a" - integrity sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ== - -"@esbuild/win32-arm64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz#7468e3692d01d629d5941e5d83817bb80f9e39b4" - integrity sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA== - -"@esbuild/win32-ia32@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz#a5bc0063fb2bcab6d0ed63f2a1537958bc269ec6" - integrity sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg== - -"@esbuild/win32-x64@0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz#10064ee44f4347b90c9a02b446bbf80a91632b12" - integrity sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A== +"@esbuild/darwin-arm64@0.27.7": + version "0.27.7" + resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz" + integrity sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw== + +"@esbuild/darwin-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz" + integrity sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw== "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.9.1" @@ -888,7 +714,7 @@ jest-haste-map "^29.7.0" slash "^3.0.0" -"@jest/transform@^29.7.0": +"@jest/transform@^29.0.0 || ^30.0.0", "@jest/transform@^29.7.0": version "29.7.0" resolved "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz" integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== @@ -909,7 +735,7 @@ slash "^3.0.0" write-file-atomic "^4.0.2" -"@jest/types@^29.6.3": +"@jest/types@^29.0.0 || ^30.0.0", "@jest/types@^29.6.3": version "29.6.3" resolved "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz" integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== @@ -957,7 +783,7 @@ "@manypkg/find-root@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@manypkg/find-root/-/find-root-1.1.0.tgz#a62d8ed1cd7e7d4c11d9d52a8397460b5d4ad29f" + resolved "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz" integrity sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA== dependencies: "@babel/runtime" "^7.5.5" @@ -967,7 +793,7 @@ "@manypkg/get-packages@^1.1.3": version "1.1.3" - resolved "https://registry.yarnpkg.com/@manypkg/get-packages/-/get-packages-1.1.3.tgz#e184db9bba792fa4693de4658cfb1463ac2c9c47" + resolved "https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-1.1.3.tgz" integrity sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A== dependencies: "@babel/runtime" "^7.5.5" @@ -985,7 +811,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": +"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -1092,25 +918,18 @@ expect "^29.0.0" pretty-format "^29.0.0" -"@types/node@*": - version "25.5.0" - resolved "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz" - integrity sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw== - dependencies: - undici-types "~7.18.0" - -"@types/node@^12.7.1": - version "12.20.55" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" - integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== - -"@types/node@^20.0.0": +"@types/node@*", "@types/node@^20.0.0", "@types/node@>=18": version "20.19.37" resolved "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz" integrity sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw== dependencies: undici-types "~6.21.0" +"@types/node@^12.7.1": + version "12.20.55" + resolved "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz" + integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== + "@types/stack-utils@^2.0.0": version "2.0.3" resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz" @@ -1226,7 +1045,7 @@ acorn-jsx@^5.3.2: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.9.0: +"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.9.0: version "8.16.0" resolved "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz" integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== @@ -1243,7 +1062,7 @@ ajv@^6.12.4: ansi-colors@^4.1.1, ansi-colors@^4.1.3: version "4.1.3" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz" integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== ansi-escapes@^4.2.1: @@ -1282,7 +1101,12 @@ ansi-styles@^5.0.0: resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== -ansi-styles@^6.0.0, ansi-styles@^6.2.1: +ansi-styles@^6.0.0: + version "6.2.3" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz" + integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg== + +ansi-styles@^6.2.1: version "6.2.3" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz" integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg== @@ -1312,7 +1136,7 @@ array-union@^2.1.0: resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -babel-jest@^29.7.0: +"babel-jest@^29.0.0 || ^30.0.0", babel-jest@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz" integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== @@ -1392,7 +1216,7 @@ baseline-browser-mapping@^2.9.0: better-path-resolve@1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/better-path-resolve/-/better-path-resolve-1.0.0.tgz#13a35a1104cdd48a7b74bf8758f96a1ee613f99d" + resolved "https://registry.npmjs.org/better-path-resolve/-/better-path-resolve-1.0.0.tgz" integrity sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g== dependencies: is-windows "^1.0.0" @@ -1407,17 +1231,17 @@ bl@^4.1.0: readable-stream "^3.4.0" brace-expansion@^1.1.7: - version "1.1.18" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.18.tgz#3ce74d89885136be1535341f8c3d4425c29a5cab" - integrity sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw== + version "1.1.13" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz" + integrity sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" brace-expansion@^2.0.2: - version "2.1.4" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.1.4.tgz#589dab11c0018d0366be64cd8bf12c8dbecc8326" - integrity sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg== + version "2.0.3" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz" + integrity sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA== dependencies: balanced-match "^1.0.0" @@ -1428,7 +1252,7 @@ braces@^3.0.3: dependencies: fill-range "^7.1.1" -browserslist@^4.24.0: +browserslist@^4.24.0, "browserslist@>= 4.21.0": version "4.28.1" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz" integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA== @@ -1665,7 +1489,7 @@ defaults@^1.0.3: detect-indent@^6.0.0: version "6.1.0" - resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" + resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz" integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== detect-newline@^3.0.0: @@ -1714,7 +1538,7 @@ emoji-regex@^8.0.0: enquirer@^2.4.1: version "2.4.1" - resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.4.1.tgz#93334b3fbd74fc7097b224ab4a8fb7e40bf4ae56" + resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz" integrity sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ== dependencies: ansi-colors "^4.1.1" @@ -1732,37 +1556,69 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +esbuild@~0.27.0: + version "0.27.7" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz" + integrity sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w== + optionalDependencies: + "@esbuild/aix-ppc64" "0.27.7" + "@esbuild/android-arm" "0.27.7" + "@esbuild/android-arm64" "0.27.7" + "@esbuild/android-x64" "0.27.7" + "@esbuild/darwin-arm64" "0.27.7" + "@esbuild/darwin-x64" "0.27.7" + "@esbuild/freebsd-arm64" "0.27.7" + "@esbuild/freebsd-x64" "0.27.7" + "@esbuild/linux-arm" "0.27.7" + "@esbuild/linux-arm64" "0.27.7" + "@esbuild/linux-ia32" "0.27.7" + "@esbuild/linux-loong64" "0.27.7" + "@esbuild/linux-mips64el" "0.27.7" + "@esbuild/linux-ppc64" "0.27.7" + "@esbuild/linux-riscv64" "0.27.7" + "@esbuild/linux-s390x" "0.27.7" + "@esbuild/linux-x64" "0.27.7" + "@esbuild/netbsd-arm64" "0.27.7" + "@esbuild/netbsd-x64" "0.27.7" + "@esbuild/openbsd-arm64" "0.27.7" + "@esbuild/openbsd-x64" "0.27.7" + "@esbuild/openharmony-arm64" "0.27.7" + "@esbuild/sunos-x64" "0.27.7" + "@esbuild/win32-arm64" "0.27.7" + "@esbuild/win32-ia32" "0.27.7" + "@esbuild/win32-x64" "0.27.7" + esbuild@~0.28.0: - version "0.28.1" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.28.1.tgz#ef45b4634c9c9d97a296aea4114a5f9840f95578" - integrity sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw== + version "0.28.2" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz" + integrity sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA== optionalDependencies: - "@esbuild/aix-ppc64" "0.28.1" - "@esbuild/android-arm" "0.28.1" - "@esbuild/android-arm64" "0.28.1" - "@esbuild/android-x64" "0.28.1" - "@esbuild/darwin-arm64" "0.28.1" - "@esbuild/darwin-x64" "0.28.1" - "@esbuild/freebsd-arm64" "0.28.1" - "@esbuild/freebsd-x64" "0.28.1" - "@esbuild/linux-arm" "0.28.1" - "@esbuild/linux-arm64" "0.28.1" - "@esbuild/linux-ia32" "0.28.1" - "@esbuild/linux-loong64" "0.28.1" - "@esbuild/linux-mips64el" "0.28.1" - "@esbuild/linux-ppc64" "0.28.1" - "@esbuild/linux-riscv64" "0.28.1" - "@esbuild/linux-s390x" "0.28.1" - "@esbuild/linux-x64" "0.28.1" - "@esbuild/netbsd-arm64" "0.28.1" - "@esbuild/netbsd-x64" "0.28.1" - "@esbuild/openbsd-arm64" "0.28.1" - "@esbuild/openbsd-x64" "0.28.1" - "@esbuild/openharmony-arm64" "0.28.1" - "@esbuild/sunos-x64" "0.28.1" - "@esbuild/win32-arm64" "0.28.1" - "@esbuild/win32-ia32" "0.28.1" - "@esbuild/win32-x64" "0.28.1" + "@esbuild/aix-ppc64" "0.28.2" + "@esbuild/android-arm" "0.28.2" + "@esbuild/android-arm64" "0.28.2" + "@esbuild/android-x64" "0.28.2" + "@esbuild/darwin-arm64" "0.28.2" + "@esbuild/darwin-x64" "0.28.2" + "@esbuild/freebsd-arm64" "0.28.2" + "@esbuild/freebsd-x64" "0.28.2" + "@esbuild/linux-arm" "0.28.2" + "@esbuild/linux-arm64" "0.28.2" + "@esbuild/linux-ia32" "0.28.2" + "@esbuild/linux-loong64" "0.28.2" + "@esbuild/linux-mips64el" "0.28.2" + "@esbuild/linux-ppc64" "0.28.2" + "@esbuild/linux-riscv64" "0.28.2" + "@esbuild/linux-s390x" "0.28.2" + "@esbuild/linux-x64" "0.28.2" + "@esbuild/netbsd-arm64" "0.28.2" + "@esbuild/netbsd-x64" "0.28.2" + "@esbuild/openbsd-arm64" "0.28.2" + "@esbuild/openbsd-x64" "0.28.2" + "@esbuild/openharmony-arm64" "0.28.2" + "@esbuild/sunos-x64" "0.28.2" + "@esbuild/win32-arm64" "0.28.2" + "@esbuild/win32-ia32" "0.28.2" + "@esbuild/win32-x64" "0.28.2" escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" @@ -1797,7 +1653,7 @@ eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint@^8.57.0: +"eslint@^6.0.0 || ^7.0.0 || >=8.0.0", eslint@^8.56.0, eslint@^8.57.0: version "8.57.1" resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz" integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== @@ -1932,7 +1788,7 @@ expect@^29.0.0, expect@^29.7.0: extendable-error@^0.1.5: version "0.1.7" - resolved "https://registry.yarnpkg.com/extendable-error/-/extendable-error-0.1.7.tgz#60b9adf206264ac920058a7395685ae4670c2b96" + resolved "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.7.tgz" integrity sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg== fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: @@ -1951,7 +1807,7 @@ fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.8" -fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: +fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0, fast-json-stable-stringify@2.x: version "2.1.0" resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -2028,7 +1884,7 @@ flatted@^3.2.9: fs-extra@^7.0.1: version "7.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz" integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== dependencies: graceful-fs "^4.1.2" @@ -2037,7 +1893,7 @@ fs-extra@^7.0.1: fs-extra@^8.1.0: version "8.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz" integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== dependencies: graceful-fs "^4.2.0" @@ -2089,6 +1945,13 @@ get-stream@^8.0.1: resolved "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz" integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== +get-tsconfig@^4.7.5: + version "4.14.0" + resolved "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz" + integrity sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA== + dependencies: + resolve-pkg-maps "^1.0.0" + glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" @@ -2136,7 +1999,7 @@ globby@^11.0.0, globby@^11.1.0: graceful-fs@^4.1.2, graceful-fs@^4.1.5, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.9: version "4.2.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== graphemer@^1.4.0: @@ -2175,7 +2038,7 @@ html-escaper@^2.0.0: human-id@^4.1.1: version "4.1.3" - resolved "https://registry.yarnpkg.com/human-id/-/human-id-4.1.3.tgz#f408633c6febbef4650758f00ffca0967afb566d" + resolved "https://registry.npmjs.org/human-id/-/human-id-4.1.3.tgz" integrity sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q== human-signals@^2.1.0: @@ -2239,14 +2102,14 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3, inherits@^2.0.4: +inherits@^2.0.3, inherits@^2.0.4, inherits@2: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== inquirer@^8.2.7: version "8.2.7" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.7.tgz#62f6b931a9b7f8735dc42db927316d8fb6f71de8" + resolved "https://registry.npmjs.org/inquirer/-/inquirer-8.2.7.tgz" integrity sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA== dependencies: "@inquirer/external-editor" "^1.0.0" @@ -2323,7 +2186,7 @@ is-number@^7.0.0: is-path-inside@^3.0.3: version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== is-stream@^2.0.0: @@ -2338,7 +2201,7 @@ is-stream@^3.0.0: is-subdir@^1.1.1: version "1.2.0" - resolved "https://registry.yarnpkg.com/is-subdir/-/is-subdir-1.2.0.tgz#b791cd28fab5202e91a08280d51d9d7254fd20d4" + resolved "https://registry.npmjs.org/is-subdir/-/is-subdir-1.2.0.tgz" integrity sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw== dependencies: better-path-resolve "1.0.0" @@ -2350,7 +2213,7 @@ is-unicode-supported@^0.1.0: is-windows@^1.0.0: version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + resolved "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== isexe@^2.0.0: @@ -2615,7 +2478,7 @@ jest-resolve-dependencies@^29.7.0: jest-regex-util "^29.6.3" jest-snapshot "^29.7.0" -jest-resolve@^29.7.0: +jest-resolve@*, jest-resolve@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz" integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== @@ -2711,7 +2574,7 @@ jest-snapshot@^29.7.0: pretty-format "^29.7.0" semver "^7.5.3" -jest-util@^29.7.0: +"jest-util@^29.0.0 || ^30.0.0", jest-util@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz" integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== @@ -2759,7 +2622,7 @@ jest-worker@^29.7.0: merge-stream "^2.0.0" supports-color "^8.0.0" -jest@^29.7.0: +"jest@^29.0.0 || ^30.0.0", jest@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz" integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== @@ -2774,18 +2637,26 @@ js-tokens@^4.0.0: resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.13.1, js-yaml@^3.6.1: - version "3.15.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.15.1.tgz#24bc95028f361cdaaa84745b06a109c4486773c0" - integrity sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag== +js-yaml@^3.13.1: + version "3.14.2" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz" + integrity sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@^3.6.1: + version "3.14.2" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz" + integrity sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg== dependencies: argparse "^1.0.7" esprima "^4.0.0" js-yaml@^4.1.0, js-yaml@^4.1.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.1.tgz#01216c001d67f48e2cd560d708c7af21090a3848" - integrity sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ== + version "4.1.1" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz" + integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== dependencies: argparse "^2.0.1" @@ -2821,7 +2692,7 @@ json5@^2.2.3: jsonfile@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz" integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== optionalDependencies: graceful-fs "^4.1.6" @@ -2915,12 +2786,12 @@ lodash.merge@^4.6.2: lodash.startcase@^4.4.0: version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.startcase/-/lodash.startcase-4.4.0.tgz#9436e34ed26093ed7ffae1936144350915d9add8" + resolved "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz" integrity sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg== lodash@^4.17.21: version "4.18.1" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz" integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== log-symbols@^4.1.0: @@ -3017,12 +2888,12 @@ minimatch@^9.0.4: minimist@^1.2.5: version "1.2.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== mri@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" + resolved "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz" integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== ms@^2.1.3: @@ -3076,7 +2947,7 @@ npm-run-path@^5.1.0: once@^1.3.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" @@ -3131,12 +3002,12 @@ ora@^5.4.1: outdent@^0.5.0: version "0.5.0" - resolved "https://registry.yarnpkg.com/outdent/-/outdent-0.5.0.tgz#9e10982fdc41492bb473ad13840d22f9655be2ff" + resolved "https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz" integrity sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q== p-filter@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/p-filter/-/p-filter-2.1.0.tgz#1b1472562ae7a0f742f0f3d3d3718ea66ff9c09c" + resolved "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz" integrity sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw== dependencies: p-map "^2.0.0" @@ -3171,7 +3042,7 @@ p-locate@^5.0.0: p-map@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" + resolved "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz" integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== p-try@^2.0.0: @@ -3181,7 +3052,7 @@ p-try@^2.0.0: package-manager-detector@^0.2.0: version "0.2.11" - resolved "https://registry.yarnpkg.com/package-manager-detector/-/package-manager-detector-0.2.11.tgz#3af0b34f99d86d24af0a0620603d2e1180d05c9c" + resolved "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.11.tgz" integrity sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ== dependencies: quansync "^0.2.7" @@ -3250,7 +3121,7 @@ pidtree@^0.6.0: pify@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + resolved "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== pirates@^4.0.4: @@ -3272,7 +3143,7 @@ prelude-ls@^1.2.1: prettier@^2.7.1: version "2.8.8" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== prettier@^3.2.0: @@ -3309,7 +3180,7 @@ pure-rand@^6.0.0: quansync@^0.2.7: version "0.2.11" - resolved "https://registry.yarnpkg.com/quansync/-/quansync-0.2.11.tgz#f9c3adda2e1272e4f8cf3f1457b04cbdb4ee692a" + resolved "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz" integrity sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA== queue-microtask@^1.2.2: @@ -3324,7 +3195,7 @@ react-is@^18.0.0: read-yaml-file@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/read-yaml-file/-/read-yaml-file-1.1.0.tgz#9362bbcbdc77007cc8ea4519fe1c0b821a7ce0d8" + resolved "https://registry.npmjs.org/read-yaml-file/-/read-yaml-file-1.1.0.tgz" integrity sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA== dependencies: graceful-fs "^4.1.5" @@ -3363,6 +3234,11 @@ resolve-from@^5.0.0: resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve-pkg-maps@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz" + integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== + resolve.exports@^2.0.0: version "2.0.3" resolved "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz" @@ -3439,14 +3315,19 @@ safe-buffer@~5.2.0: resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -semver@^6.3.0, semver@^6.3.1: +semver@^6.3.0: + version "6.3.1" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^6.3.1: version "6.3.1" resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.7.3: version "7.7.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" + resolved "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz" integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== shebang-command@^2.0.0: @@ -3466,7 +3347,12 @@ signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== -signal-exit@^4.0.1, signal-exit@^4.1.0: +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +signal-exit@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== @@ -3512,7 +3398,7 @@ source-map@^0.6.0, source-map@^0.6.1: spawndamnit@^3.0.1: version "3.0.1" - resolved "https://registry.yarnpkg.com/spawndamnit/-/spawndamnit-3.0.1.tgz#44410235d3dc4e21f8e4f740ae3266e4486c2aed" + resolved "https://registry.npmjs.org/spawndamnit/-/spawndamnit-3.0.1.tgz" integrity sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg== dependencies: cross-spawn "^7.0.5" @@ -3530,6 +3416,13 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + string-argv@^0.3.2: version "0.3.2" resolved "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz" @@ -3561,13 +3454,6 @@ string-width@^7.0.0: get-east-asian-width "^1.0.0" strip-ansi "^7.1.0" -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" @@ -3584,7 +3470,7 @@ strip-ansi@^7.1.0: strip-bom@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== strip-bom@^4.0.0: @@ -3628,7 +3514,7 @@ supports-preserve-symlinks-flag@^1.0.0: term-size@^2.1.0: version "2.2.1" - resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54" + resolved "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz" integrity sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg== test-exclude@^6.0.0: @@ -3688,11 +3574,12 @@ tslib@^2.1.0: integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== tsx@^4.19.0: - version "4.23.1" - resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.23.1.tgz#1703da002ee4432b9a053917431f91462fca235b" - integrity sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ== + version "4.21.0" + resolved "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz" + integrity sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw== dependencies: - esbuild "~0.28.0" + esbuild "~0.27.0" + get-tsconfig "^4.7.5" optionalDependencies: fsevents "~2.3.3" @@ -3723,7 +3610,7 @@ type-fest@^4.41.0: resolved "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz" integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA== -typescript@^5.7.0: +typescript@^5.7.0, typescript@>=4.2.0, "typescript@>=4.3 <6": version "5.9.3" resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz" integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== @@ -3745,7 +3632,7 @@ undici-types@~7.18.0: universalify@^0.1.0: version "0.1.2" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== update-browserslist-db@^1.2.0: From 6322f3cbe2b1e7b5b8533d1729e3ea539af5c8ca Mon Sep 17 00:00:00 2001 From: Satyammittal1011 Date: Fri, 14 Aug 2026 17:51:11 +0530 Subject: [PATCH 2/3] feat: gate Brevo Function behind __BREVO_PREVIEW__ and add fn alias - Add `brevo-function-type` to FEATURE_STAGE as a preview feature - Move function group definition to preview-definitions.ts so esbuild can eliminate it from published builds - Gate the Brevo Function choice in `app create` behind __BREVO_PREVIEW__ - Gate the Function commands section in help.ts behind __BREVO_PREVIEW__ - Add `aliases` support to SubcommandGroupDefinition so `brevo fn list` and `brevo fn get` work as shortcuts - Update tests for the conditional export and gated app-type prompt Co-Authored-By: Claude Opus 4.6 --- src/__tests__/commands/app/create.test.ts | 7 +--- src/__tests__/commands/definitions.test.ts | 14 +++++-- src/__tests__/lib/help.test.ts | 11 +++-- src/__tests__/lib/preview.test.ts | 1 + src/bin/index.ts | 6 ++- src/commands/app/create.ts | 11 +++-- src/commands/definitions.ts | 48 ++++++---------------- src/commands/preview-definitions.ts | 36 +++++++++++++++- src/lib/command-registry.ts | 7 ++++ src/lib/help.ts | 12 ++++-- src/lib/preview.ts | 5 ++- 11 files changed, 97 insertions(+), 61 deletions(-) diff --git a/src/__tests__/commands/app/create.test.ts b/src/__tests__/commands/app/create.test.ts index e4b196d..7a32792 100644 --- a/src/__tests__/commands/app/create.test.ts +++ b/src/__tests__/commands/app/create.test.ts @@ -2540,14 +2540,11 @@ describe('app/create', () => { // assertion below is a `not.toContain`, which an unnoticed indent would satisfy // vacuously, quietly turning the gate's own test green for the wrong reason. const labels = appTypeQuestion.choices.map((choice: { name: string }) => choice.name.trim()); - expect(labels).toEqual([ - messages.APP_CREATE_APP_TYPE_OAUTH, - messages.APP_CREATE_APP_TYPE_FUNCTION, - ]); + expect(labels).toEqual([messages.APP_CREATE_APP_TYPE_OAUTH]); expect(labels).not.toContain(messages.APP_CREATE_APP_TYPE_UI); + expect(labels).not.toContain(messages.APP_CREATE_APP_TYPE_FUNCTION); expect(appTypeQuestion.choices.map((choice: { value: string }) => choice.value)).toEqual([ 'oauth', - 'function', ]); const payload = (appService.createApp as jest.Mock).mock.calls[0][0]; diff --git a/src/__tests__/commands/definitions.test.ts b/src/__tests__/commands/definitions.test.ts index f652e8e..ca61dfe 100644 --- a/src/__tests__/commands/definitions.test.ts +++ b/src/__tests__/commands/definitions.test.ts @@ -14,15 +14,21 @@ describe('appCommandGroup', () => { }); }); +// Tests run with __BREVO_PREVIEW__ = true (jest.setup.js), so functionCommandGroup +// is defined. The conditional export is tested by the preview-gate suite. describe('functionCommandGroup', () => { + it('is defined in a preview build', () => { + expect(functionCommandGroup).toBeDefined(); + }); + it('registers list and get subcommands', () => { - const names = functionCommandGroup.commands.map((c) => c.name); + const names = functionCommandGroup!.commands.map((c) => c.name); expect(names).toContain('list'); expect(names).toContain('get'); }); it('list command supports --json and --draft', () => { - const cmd = functionCommandGroup.commands.find((c) => c.name === 'list'); + const cmd = functionCommandGroup!.commands.find((c) => c.name === 'list'); expect(cmd).toBeDefined(); const flags = (cmd!.options ?? []).map((o) => o.flags); expect(flags).toContain('--json'); @@ -30,14 +36,14 @@ describe('functionCommandGroup', () => { }); it('get command supports --json', () => { - const cmd = functionCommandGroup.commands.find((c) => c.name === 'get'); + const cmd = functionCommandGroup!.commands.find((c) => c.name === 'get'); expect(cmd).toBeDefined(); const flags = (cmd!.options ?? []).map((o) => o.flags); expect(flags).toContain('--json'); }); it('get command takes an argument', () => { - const cmd = functionCommandGroup.commands.find((c) => c.name === 'get'); + const cmd = functionCommandGroup!.commands.find((c) => c.name === 'get'); expect(cmd).toBeDefined(); expect(cmd!.arguments).toBeDefined(); expect(cmd!.arguments!.some((a) => a.name.includes('id'))).toBe(true); diff --git a/src/__tests__/lib/help.test.ts b/src/__tests__/lib/help.test.ts index 2dddf87..286092b 100644 --- a/src/__tests__/lib/help.test.ts +++ b/src/__tests__/lib/help.test.ts @@ -18,11 +18,9 @@ function buildProgram(): Command { .version('0.0.0-test') .option('--debug', 'Enable debug logging') .configureHelp({ formatHelp: createHelpFormatter(program) }); - registerAll(program, topLevelCommands, [ - appCommandGroup, - skillCommandGroup, - functionCommandGroup, - ]); + const groups = [appCommandGroup, skillCommandGroup]; + if (functionCommandGroup) groups.push(functionCommandGroup); + registerAll(program, topLevelCommands, groups); return program; } @@ -110,7 +108,8 @@ describe('help formatting', () => { it('gives every registered subcommand its own usage line', () => { const program = buildProgram(); - const groups = [appCommandGroup, skillCommandGroup, functionCommandGroup]; + const groups = [appCommandGroup, skillCommandGroup]; + if (functionCommandGroup) groups.push(functionCommandGroup); for (const group of groups) { for (const cmd of group.commands) { diff --git a/src/__tests__/lib/preview.test.ts b/src/__tests__/lib/preview.test.ts index 467f646..c1fb975 100644 --- a/src/__tests__/lib/preview.test.ts +++ b/src/__tests__/lib/preview.test.ts @@ -31,6 +31,7 @@ describe('lib/preview', () => { 'review-lifecycle': 'preview', 'ui-app-type': 'preview', 'public-distribution': 'preview', + 'brevo-function-type': 'preview', }); }); }); diff --git a/src/bin/index.ts b/src/bin/index.ts index d199a1b..a048630 100644 --- a/src/bin/index.ts +++ b/src/bin/index.ts @@ -16,7 +16,7 @@ import { refreshAccessToken, RefreshError } from '../services/oauth-refresh'; import { stopActiveSpinner } from '../lib/ui'; import { AccountResponse } from '../types'; import { client } from '../container'; -import { registerAll } from '../lib/command-registry'; +import { registerAll, SubcommandGroupDefinition } from '../lib/command-registry'; import { topLevelCommands, appCommandGroup, @@ -110,7 +110,9 @@ client.setEnsureFresh(async () => { // ──────────────── Register all commands ──────────────── -registerAll(program, topLevelCommands, [appCommandGroup, skillCommandGroup, functionCommandGroup]); +const commandGroups: SubcommandGroupDefinition[] = [appCommandGroup, skillCommandGroup]; +if (functionCommandGroup) commandGroups.push(functionCommandGroup); +registerAll(program, topLevelCommands, commandGroups); // ──────────────── Re-auth handler ──────────────── diff --git a/src/commands/app/create.ts b/src/commands/app/create.ts index fdd2027..cd45180 100644 --- a/src/commands/app/create.ts +++ b/src/commands/app/create.ts @@ -136,9 +136,14 @@ async function resolveAppType(interactive: boolean, distribution?: string): Prom if (__BREVO_PREVIEW__ && isFeatureAvailable('ui-app-type')) { choices.push({ name: messages.APP_CREATE_APP_TYPE_UI, value: 'ui' }); } - // Brevo Function is offered only for private apps — not behind __BREVO_PREVIEW__, - // since it ships in the published build. - if (distribution === 'private') { + // ELIMINATION SITE — same pattern as the UI-app choice: the raw global lets esbuild + // fold this branch away in a published build. `isFeatureAvailable` is still consulted + // so flipping `FEATURE_STAGE['brevo-function-type']` to `'ga'` releases the choice. + if ( + __BREVO_PREVIEW__ && + isFeatureAvailable('brevo-function-type') && + distribution === 'private' + ) { choices.push({ name: messages.APP_CREATE_APP_TYPE_FUNCTION, value: 'function' }); } const answer = await inquirer.prompt([ diff --git a/src/commands/definitions.ts b/src/commands/definitions.ts index a184b96..0050721 100644 --- a/src/commands/definitions.ts +++ b/src/commands/definitions.ts @@ -3,10 +3,10 @@ import { parseAppId, parsePositiveInt, collectUrls, validateUrl } from '../lib/v import { isFeatureAvailable } from '../lib/preview'; import { createDescription, distributionValues } from '../lib/help'; // The gated subcommands are referenced only through this binding, and only from behind -// `__BREVO_PREVIEW__`. That is what lets esbuild drop them — and their five handler -// modules — from a published build. Importing any of those handlers directly here would -// make them live references again and ship the whole surface. See ./preview-definitions.ts. -import { previewAppCommands } from './preview-definitions'; +// `__BREVO_PREVIEW__`. That is what lets esbuild drop them — and their handler modules — +// from a published build. Importing any of those handlers directly here would make them +// live references again and ship the whole surface. See ./preview-definitions.ts. +import { previewAppCommands, previewFunctionGroup } from './preview-definitions'; import { initCommand } from './init'; import { loginCommand } from './login'; @@ -22,8 +22,6 @@ import { scopesCommand } from './app/scopes'; import { startCommand } from './app/start'; import { installCommand as skillInstallCommand } from './skill/install'; import { uninstallCommand as skillUninstallCommand } from './skill/uninstall'; -import { listFunctionCommand } from './function/list'; -import { getFunctionCommand } from './function/get'; export const topLevelCommands: CommandDefinition[] = [ { @@ -283,32 +281,12 @@ export const skillCommandGroup: SubcommandGroupDefinition = { ], }; -export const functionCommandGroup: SubcommandGroupDefinition = { - name: 'function', - description: 'Manage Brevo Functions', - commands: [ - { - name: 'list', - description: 'List all Brevo Functions in your account', - examples: [ - 'brevo function list', - 'brevo function list --draft', - 'brevo function list --json', - ], - options: [ - { flags: '--draft', description: 'List only draft functions' }, - { flags: '--json', description: 'Output as JSON' }, - ], - handler: (opts) => - listFunctionCommand({ json: Boolean(opts.json), draft: Boolean(opts.draft) }), - }, - { - name: 'get', - description: 'Show details of a Brevo Function', - arguments: [{ name: '', description: 'Function ID' }], - examples: ['brevo function get fn-001', 'brevo function get fn-001 --json'], - options: [{ flags: '--json', description: 'Output as JSON' }], - handler: (opts, id) => getFunctionCommand({ id: id as string, json: Boolean(opts.json) }), - }, - ], -}; +// ELIMINATION SITE — same pattern as previewAppCommands above: the raw global lets +// esbuild fold this to `undefined` in a published build and tree-shake the function +// handler modules only `previewFunctionGroup` references. `isFeatureAvailable` is +// still consulted at runtime (for the help screen, via `gatedSection`), so flipping +// `FEATURE_STAGE['brevo-function-type']` to `'ga'` releases the group without +// touching this line. +export const functionCommandGroup: SubcommandGroupDefinition | undefined = __BREVO_PREVIEW__ + ? previewFunctionGroup + : undefined; diff --git a/src/commands/preview-definitions.ts b/src/commands/preview-definitions.ts index 5734ddc..0a0a04e 100644 --- a/src/commands/preview-definitions.ts +++ b/src/commands/preview-definitions.ts @@ -16,7 +16,7 @@ * At GA, move the released entries back into `definitions.ts` and delete this file * when it empties. See `RELEASE-CHECKLIST.md`. */ -import type { CommandDefinition } from '../lib/command-registry'; +import type { CommandDefinition, SubcommandGroupDefinition } from '../lib/command-registry'; import { parseAppId } from '../lib/validators'; import { deployCommand } from './app/deploy'; @@ -24,6 +24,8 @@ import { rollbackCommand } from './app/rollback'; import { statusCommand } from './app/status'; import { submitCommand } from './app/submit'; import { withdrawCommand } from './app/withdraw'; +import { listFunctionCommand } from './function/list'; +import { getFunctionCommand } from './function/get'; /** The `brevo app ` subcommands gated behind an unreleased feature. */ export const previewAppCommands: CommandDefinition[] = [ @@ -175,3 +177,35 @@ export const previewAppCommands: CommandDefinition[] = [ }), }, ]; + +/** The `brevo function` group, gated behind the preview build. */ +export const previewFunctionGroup: SubcommandGroupDefinition = { + name: 'function', + aliases: ['fn'], + description: 'Manage Brevo Functions', + commands: [ + { + name: 'list', + description: 'List all Brevo Functions in your account', + examples: [ + 'brevo function list', + 'brevo function list --draft', + 'brevo function list --json', + ], + options: [ + { flags: '--draft', description: 'List only draft functions' }, + { flags: '--json', description: 'Output as JSON' }, + ], + handler: (opts) => + listFunctionCommand({ json: Boolean(opts.json), draft: Boolean(opts.draft) }), + }, + { + name: 'get', + description: 'Show details of a Brevo Function', + arguments: [{ name: '', description: 'Function ID' }], + examples: ['brevo function get fn-001', 'brevo function get fn-001 --json'], + options: [{ flags: '--json', description: 'Output as JSON' }], + handler: (opts, id) => getFunctionCommand({ id: id as string, json: Boolean(opts.json) }), + }, + ], +}; diff --git a/src/lib/command-registry.ts b/src/lib/command-registry.ts index 209d8ea..904c738 100644 --- a/src/lib/command-registry.ts +++ b/src/lib/command-registry.ts @@ -60,6 +60,8 @@ export interface CommandDefinition { export interface SubcommandGroupDefinition { name: string; + /** Alternative names that resolve to the same group (e.g. `fn` for `function`). */ + aliases?: string[]; description: string; commands: CommandDefinition[]; } @@ -181,6 +183,11 @@ function registerRemovedCommand(parent: Command, removed: RemovedCommand): void */ function registerSubcommandGroup(parent: Command, group: SubcommandGroupDefinition): void { const groupCmd = parent.command(group.name).description(group.description); + if (group.aliases) { + for (const alias of group.aliases) { + groupCmd.alias(alias); + } + } for (const def of group.commands) { registerCommand(groupCmd, def); } diff --git a/src/lib/help.ts b/src/lib/help.ts index c9a0657..41a8916 100644 --- a/src/lib/help.ts +++ b/src/lib/help.ts @@ -114,10 +114,14 @@ function formatRootHelp(description: string): string { ` brevo skill:cli install [--json] Install the brevo-cli Claude Code skill`, ` brevo skill:cli uninstall [--json] Remove the brevo-cli skill`, ``, - `Function commands:`, - ` brevo function list [--draft] [--json] List all Brevo Functions in your account`, - ` brevo function get [--json] Show details of a Brevo Function`, - ``, + ...(__BREVO_PREVIEW__ + ? gatedSection('brevo-function-type', [ + `Function commands:`, + ` brevo function list [--draft] [--json] List all Brevo Functions in your account`, + ` brevo function get [--json] Show details of a Brevo Function`, + ``, + ]) + : []), `Scope commands:`, ` brevo app available-scopes [--web] [--json] List OAuth scopes supported by the IdP`, ` (--web opens the catalog in a browser)`, diff --git a/src/lib/preview.ts b/src/lib/preview.ts index 5d53b6b..60c9248 100644 --- a/src/lib/preview.ts +++ b/src/lib/preview.ts @@ -47,7 +47,9 @@ export type PreviewFeature = /** The *UI app* choice in `app create`'s app-type prompt. */ | 'ui-app-type' /** `app create --distribution public`. */ - | 'public-distribution'; + | 'public-distribution' + /** The *Brevo Function* choice in `app create` and the `brevo function` commands. */ + | 'brevo-function-type'; export type FeatureStage = 'ga' | 'preview'; @@ -62,6 +64,7 @@ export const FEATURE_STAGE: Readonly> = { 'review-lifecycle': 'preview', 'ui-app-type': 'preview', 'public-distribution': 'preview', + 'brevo-function-type': 'preview', } as const; /** From 255350a8431234aadc642c4fc3431f39091bb19b Mon Sep 17 00:00:00 2001 From: Satyammittal1011 Date: Mon, 17 Aug 2026 13:05:34 +0530 Subject: [PATCH 3/3] feat: add brevo function activate, deactivate and delete commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add three management commands to the Brevo Function group: - `brevo function activate ` — PATCH with is_active: true - `brevo function deactivate ` — PATCH with is_active: false - `brevo function delete ` — DELETE with --force to skip confirmation All three are gated behind __BREVO_PREVIEW__ alongside the existing list and get commands. Co-Authored-By: Claude Opus 4.6 --- src/__tests__/commands/definitions.test.ts | 33 ++++- .../commands/function/activate.test.ts | 86 ++++++++++++ .../commands/function/deactivate.test.ts | 86 ++++++++++++ .../commands/function/delete.test.ts | 123 ++++++++++++++++++ src/__tests__/services/function.test.ts | 84 ++++++++++++ src/commands/function/activate.ts | 38 ++++++ src/commands/function/deactivate.ts | 38 ++++++ src/commands/function/delete.ts | 54 ++++++++ src/commands/preview-definitions.ts | 41 ++++++ src/lang/preview-messages.ts | 15 +++ src/lib/constants.ts | 3 + src/services/function.ts | 12 ++ 12 files changed, 612 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/commands/function/activate.test.ts create mode 100644 src/__tests__/commands/function/deactivate.test.ts create mode 100644 src/__tests__/commands/function/delete.test.ts create mode 100644 src/commands/function/activate.ts create mode 100644 src/commands/function/deactivate.ts create mode 100644 src/commands/function/delete.ts diff --git a/src/__tests__/commands/definitions.test.ts b/src/__tests__/commands/definitions.test.ts index ca61dfe..0fd0d7e 100644 --- a/src/__tests__/commands/definitions.test.ts +++ b/src/__tests__/commands/definitions.test.ts @@ -21,10 +21,13 @@ describe('functionCommandGroup', () => { expect(functionCommandGroup).toBeDefined(); }); - it('registers list and get subcommands', () => { + it('registers list, get, activate, deactivate and delete subcommands', () => { const names = functionCommandGroup!.commands.map((c) => c.name); expect(names).toContain('list'); expect(names).toContain('get'); + expect(names).toContain('activate'); + expect(names).toContain('deactivate'); + expect(names).toContain('delete'); }); it('list command supports --json and --draft', () => { @@ -48,4 +51,32 @@ describe('functionCommandGroup', () => { expect(cmd!.arguments).toBeDefined(); expect(cmd!.arguments!.some((a) => a.name.includes('id'))).toBe(true); }); + + it('activate command supports --json and takes ', () => { + const cmd = functionCommandGroup!.commands.find((c) => c.name === 'activate'); + expect(cmd).toBeDefined(); + const flags = (cmd!.options ?? []).map((o) => o.flags); + expect(flags).toContain('--json'); + expect(cmd!.arguments).toBeDefined(); + expect(cmd!.arguments!.some((a) => a.name.includes('id'))).toBe(true); + }); + + it('deactivate command supports --json and takes ', () => { + const cmd = functionCommandGroup!.commands.find((c) => c.name === 'deactivate'); + expect(cmd).toBeDefined(); + const flags = (cmd!.options ?? []).map((o) => o.flags); + expect(flags).toContain('--json'); + expect(cmd!.arguments).toBeDefined(); + expect(cmd!.arguments!.some((a) => a.name.includes('id'))).toBe(true); + }); + + it('delete command supports --force, --json and takes ', () => { + const cmd = functionCommandGroup!.commands.find((c) => c.name === 'delete'); + expect(cmd).toBeDefined(); + const flags = (cmd!.options ?? []).map((o) => o.flags); + expect(flags).toContain('--force'); + expect(flags).toContain('--json'); + expect(cmd!.arguments).toBeDefined(); + expect(cmd!.arguments!.some((a) => a.name.includes('id'))).toBe(true); + }); }); diff --git a/src/__tests__/commands/function/activate.test.ts b/src/__tests__/commands/function/activate.test.ts new file mode 100644 index 0000000..bbc31e8 --- /dev/null +++ b/src/__tests__/commands/function/activate.test.ts @@ -0,0 +1,86 @@ +import { activateFunctionCommand } from '../../../commands/function/activate'; +import { ApiError } from '../../../lib/errors'; + +jest.mock('../../../container', () => ({ + functionService: { + activateFunction: jest.fn(), + }, +})); + +import { functionService } from '../../../container'; + +describe('function/activate', () => { + let stdoutSpy: jest.SpyInstance; + + beforeEach(() => { + stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true); + jest.clearAllMocks(); + }); + + afterEach(() => { + stdoutSpy.mockRestore(); + }); + + it('should show success message on activation', async () => { + (functionService.activateFunction as jest.Mock).mockResolvedValue(undefined); + + await activateFunctionCommand({ id: 'fn-001', json: false }); + + expect(functionService.activateFunction).toHaveBeenCalledWith('fn-001'); + const output = stdoutSpy.mock.calls.map((c: [string]) => c[0]).join(''); + expect(output).toContain('Brevo Function "fn-001" activated'); + }); + + it('should output JSON on success with --json', async () => { + (functionService.activateFunction as jest.Mock).mockResolvedValue(undefined); + + await activateFunctionCommand({ id: 'fn-001', json: true }); + + const output = stdoutSpy.mock.calls[0][0]; + const parsed = JSON.parse(output); + expect(parsed.activated).toBe(true); + expect(parsed.id).toBe('fn-001'); + }); + + it('should show not-found message on 404', async () => { + (functionService.activateFunction as jest.Mock).mockRejectedValue( + new ApiError('Not found', 404), + ); + + await activateFunctionCommand({ id: 'fn-999', json: false }); + + const output = stdoutSpy.mock.calls.map((c: [string]) => c[0]).join(''); + expect(output).toContain('Brevo Function "fn-999" not found'); + }); + + it('should output JSON error on 404 with --json', async () => { + (functionService.activateFunction as jest.Mock).mockRejectedValue( + new ApiError('Not found', 404), + ); + + await activateFunctionCommand({ id: 'fn-999', json: true }); + + const output = stdoutSpy.mock.calls[0][0]; + const parsed = JSON.parse(output); + expect(parsed.error).toBe('not_found'); + expect(parsed.message).toContain('fn-999'); + }); + + it('should propagate non-404 API errors', async () => { + (functionService.activateFunction as jest.Mock).mockRejectedValue( + new ApiError('Server error', 500), + ); + + await expect(activateFunctionCommand({ id: 'fn-001', json: false })).rejects.toThrow( + 'Server error', + ); + }); + + it('should propagate generic errors', async () => { + (functionService.activateFunction as jest.Mock).mockRejectedValue(new Error('Network error')); + + await expect(activateFunctionCommand({ id: 'fn-001', json: false })).rejects.toThrow( + 'Network error', + ); + }); +}); diff --git a/src/__tests__/commands/function/deactivate.test.ts b/src/__tests__/commands/function/deactivate.test.ts new file mode 100644 index 0000000..ee5224c --- /dev/null +++ b/src/__tests__/commands/function/deactivate.test.ts @@ -0,0 +1,86 @@ +import { deactivateFunctionCommand } from '../../../commands/function/deactivate'; +import { ApiError } from '../../../lib/errors'; + +jest.mock('../../../container', () => ({ + functionService: { + deactivateFunction: jest.fn(), + }, +})); + +import { functionService } from '../../../container'; + +describe('function/deactivate', () => { + let stdoutSpy: jest.SpyInstance; + + beforeEach(() => { + stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true); + jest.clearAllMocks(); + }); + + afterEach(() => { + stdoutSpy.mockRestore(); + }); + + it('should show success message on deactivation', async () => { + (functionService.deactivateFunction as jest.Mock).mockResolvedValue(undefined); + + await deactivateFunctionCommand({ id: 'fn-001', json: false }); + + expect(functionService.deactivateFunction).toHaveBeenCalledWith('fn-001'); + const output = stdoutSpy.mock.calls.map((c: [string]) => c[0]).join(''); + expect(output).toContain('Brevo Function "fn-001" deactivated'); + }); + + it('should output JSON on success with --json', async () => { + (functionService.deactivateFunction as jest.Mock).mockResolvedValue(undefined); + + await deactivateFunctionCommand({ id: 'fn-001', json: true }); + + const output = stdoutSpy.mock.calls[0][0]; + const parsed = JSON.parse(output); + expect(parsed.deactivated).toBe(true); + expect(parsed.id).toBe('fn-001'); + }); + + it('should show not-found message on 404', async () => { + (functionService.deactivateFunction as jest.Mock).mockRejectedValue( + new ApiError('Not found', 404), + ); + + await deactivateFunctionCommand({ id: 'fn-999', json: false }); + + const output = stdoutSpy.mock.calls.map((c: [string]) => c[0]).join(''); + expect(output).toContain('Brevo Function "fn-999" not found'); + }); + + it('should output JSON error on 404 with --json', async () => { + (functionService.deactivateFunction as jest.Mock).mockRejectedValue( + new ApiError('Not found', 404), + ); + + await deactivateFunctionCommand({ id: 'fn-999', json: true }); + + const output = stdoutSpy.mock.calls[0][0]; + const parsed = JSON.parse(output); + expect(parsed.error).toBe('not_found'); + expect(parsed.message).toContain('fn-999'); + }); + + it('should propagate non-404 API errors', async () => { + (functionService.deactivateFunction as jest.Mock).mockRejectedValue( + new ApiError('Server error', 500), + ); + + await expect(deactivateFunctionCommand({ id: 'fn-001', json: false })).rejects.toThrow( + 'Server error', + ); + }); + + it('should propagate generic errors', async () => { + (functionService.deactivateFunction as jest.Mock).mockRejectedValue(new Error('Network error')); + + await expect(deactivateFunctionCommand({ id: 'fn-001', json: false })).rejects.toThrow( + 'Network error', + ); + }); +}); diff --git a/src/__tests__/commands/function/delete.test.ts b/src/__tests__/commands/function/delete.test.ts new file mode 100644 index 0000000..c00219b --- /dev/null +++ b/src/__tests__/commands/function/delete.test.ts @@ -0,0 +1,123 @@ +import { deleteFunctionCommand } from '../../../commands/function/delete'; +import { ApiError } from '../../../lib/errors'; + +jest.mock('inquirer', () => ({ + prompt: jest.fn(), +})); + +jest.mock('../../../container', () => ({ + functionService: { + deleteFunction: jest.fn(), + }, +})); + +import inquirer from 'inquirer'; +import { functionService } from '../../../container'; + +describe('function/delete', () => { + let stdoutSpy: jest.SpyInstance; + + beforeEach(() => { + stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true); + jest.clearAllMocks(); + }); + + afterEach(() => { + stdoutSpy.mockRestore(); + }); + + it('should delete with --force without prompting', async () => { + (functionService.deleteFunction as jest.Mock).mockResolvedValue(undefined); + + await deleteFunctionCommand({ id: 'fn-001', force: true, json: false }); + + expect(inquirer.prompt).not.toHaveBeenCalled(); + expect(functionService.deleteFunction).toHaveBeenCalledWith('fn-001'); + const output = stdoutSpy.mock.calls.map((c: [string]) => c[0]).join(''); + expect(output).toContain('Brevo Function "fn-001" deleted'); + }); + + it('should output JSON with --force --json', async () => { + (functionService.deleteFunction as jest.Mock).mockResolvedValue(undefined); + + await deleteFunctionCommand({ id: 'fn-001', force: true, json: true }); + + expect(inquirer.prompt).not.toHaveBeenCalled(); + const output = stdoutSpy.mock.calls[0][0]; + const parsed = JSON.parse(output); + expect(parsed.deleted).toBe(true); + expect(parsed.id).toBe('fn-001'); + }); + + it('should delete when user confirms', async () => { + (inquirer.prompt as unknown as jest.Mock).mockResolvedValue({ confirmed: true }); + (functionService.deleteFunction as jest.Mock).mockResolvedValue(undefined); + + await deleteFunctionCommand({ id: 'fn-001', force: false, json: false }); + + expect(inquirer.prompt).toHaveBeenCalled(); + expect(functionService.deleteFunction).toHaveBeenCalledWith('fn-001'); + const output = stdoutSpy.mock.calls.map((c: [string]) => c[0]).join(''); + expect(output).toContain('Brevo Function "fn-001" deleted'); + }); + + it('should cancel when user declines', async () => { + (inquirer.prompt as unknown as jest.Mock).mockResolvedValue({ confirmed: false }); + + await deleteFunctionCommand({ id: 'fn-001', force: false, json: false }); + + expect(functionService.deleteFunction).not.toHaveBeenCalled(); + const output = stdoutSpy.mock.calls.map((c: [string]) => c[0]).join(''); + expect(output).toContain('Deletion cancelled'); + }); + + it('should skip prompt with --json (implied force)', async () => { + (functionService.deleteFunction as jest.Mock).mockResolvedValue(undefined); + + await deleteFunctionCommand({ id: 'fn-001', force: false, json: true }); + + expect(inquirer.prompt).not.toHaveBeenCalled(); + expect(functionService.deleteFunction).toHaveBeenCalledWith('fn-001'); + const output = stdoutSpy.mock.calls[0][0]; + const parsed = JSON.parse(output); + expect(parsed.deleted).toBe(true); + }); + + it('should show not-found message on 404', async () => { + (functionService.deleteFunction as jest.Mock).mockRejectedValue(new ApiError('Not found', 404)); + + await deleteFunctionCommand({ id: 'fn-999', force: true, json: false }); + + const output = stdoutSpy.mock.calls.map((c: [string]) => c[0]).join(''); + expect(output).toContain('Brevo Function "fn-999" not found'); + }); + + it('should output JSON error on 404 with --json', async () => { + (functionService.deleteFunction as jest.Mock).mockRejectedValue(new ApiError('Not found', 404)); + + await deleteFunctionCommand({ id: 'fn-999', force: true, json: true }); + + const output = stdoutSpy.mock.calls[0][0]; + const parsed = JSON.parse(output); + expect(parsed.error).toBe('not_found'); + expect(parsed.message).toContain('fn-999'); + }); + + it('should propagate non-404 API errors', async () => { + (functionService.deleteFunction as jest.Mock).mockRejectedValue( + new ApiError('Server error', 500), + ); + + await expect(deleteFunctionCommand({ id: 'fn-001', force: true, json: false })).rejects.toThrow( + 'Server error', + ); + }); + + it('should propagate generic errors', async () => { + (functionService.deleteFunction as jest.Mock).mockRejectedValue(new Error('Network error')); + + await expect(deleteFunctionCommand({ id: 'fn-001', force: true, json: false })).rejects.toThrow( + 'Network error', + ); + }); +}); diff --git a/src/__tests__/services/function.test.ts b/src/__tests__/services/function.test.ts index 6bfe559..71a0184 100644 --- a/src/__tests__/services/function.test.ts +++ b/src/__tests__/services/function.test.ts @@ -116,4 +116,88 @@ describe('services/function', () => { await expect(service.fetchFunction('fn-999')).rejects.toThrow('Not found'); }); }); + + describe('activateFunction', () => { + it('should call client.patch with is_active: true', async () => { + (mockClient.patch as jest.Mock).mockResolvedValue(undefined); + + await service.activateFunction('fn-001'); + + expect(mockClient.patch).toHaveBeenCalledWith('/v3/dp-functions/functions/fn-001', { + is_active: true, + }); + }); + + it('should encode the function ID in the URL', async () => { + (mockClient.patch as jest.Mock).mockResolvedValue(undefined); + + await service.activateFunction('fn/special id'); + + expect(mockClient.patch).toHaveBeenCalledWith( + '/v3/dp-functions/functions/fn%2Fspecial%20id', + { is_active: true }, + ); + }); + + it('should propagate API errors', async () => { + (mockClient.patch as jest.Mock).mockRejectedValue(new Error('Forbidden')); + + await expect(service.activateFunction('fn-001')).rejects.toThrow('Forbidden'); + }); + }); + + describe('deactivateFunction', () => { + it('should call client.patch with is_active: false', async () => { + (mockClient.patch as jest.Mock).mockResolvedValue(undefined); + + await service.deactivateFunction('fn-001'); + + expect(mockClient.patch).toHaveBeenCalledWith('/v3/dp-functions/functions/fn-001', { + is_active: false, + }); + }); + + it('should encode the function ID in the URL', async () => { + (mockClient.patch as jest.Mock).mockResolvedValue(undefined); + + await service.deactivateFunction('fn/special id'); + + expect(mockClient.patch).toHaveBeenCalledWith( + '/v3/dp-functions/functions/fn%2Fspecial%20id', + { is_active: false }, + ); + }); + + it('should propagate API errors', async () => { + (mockClient.patch as jest.Mock).mockRejectedValue(new Error('Forbidden')); + + await expect(service.deactivateFunction('fn-001')).rejects.toThrow('Forbidden'); + }); + }); + + describe('deleteFunction', () => { + it('should call client.delete with the function endpoint', async () => { + (mockClient.delete as jest.Mock).mockResolvedValue(undefined); + + await service.deleteFunction('fn-001'); + + expect(mockClient.delete).toHaveBeenCalledWith('/v3/dp-functions/functions/fn-001'); + }); + + it('should encode the function ID in the URL', async () => { + (mockClient.delete as jest.Mock).mockResolvedValue(undefined); + + await service.deleteFunction('fn/special id'); + + expect(mockClient.delete).toHaveBeenCalledWith( + '/v3/dp-functions/functions/fn%2Fspecial%20id', + ); + }); + + it('should propagate API errors', async () => { + (mockClient.delete as jest.Mock).mockRejectedValue(new Error('Forbidden')); + + await expect(service.deleteFunction('fn-001')).rejects.toThrow('Forbidden'); + }); + }); }); diff --git a/src/commands/function/activate.ts b/src/commands/function/activate.ts new file mode 100644 index 0000000..a0aa517 --- /dev/null +++ b/src/commands/function/activate.ts @@ -0,0 +1,38 @@ +import { logSuccess, logWarn } from '../../lib/logger'; +import { messages } from '../../lang/en'; +import { functionService } from '../../container'; +import { withCommandHandler } from '../../lib/command-handler'; +import { jsonOutput } from '../../lib/json-output'; +import { createSpinner } from '../../lib/ui'; +import { ApiError } from '../../lib/errors'; + +export const activateFunctionCommand = withCommandHandler( + async (options: { id: string; json?: boolean }): Promise => { + const spinner = createSpinner('Activating Brevo Function...', { silent: options.json }); + try { + await functionService.activateFunction(options.id); + } catch (err) { + if (err instanceof ApiError && err.statusCode === 404) { + spinner.stop(); + if (options.json) { + jsonOutput({ + error: 'not_found', + message: messages.FUNCTION_ACTIVATE_NOT_FOUND(options.id), + }); + return; + } + logWarn(`\n ${messages.FUNCTION_ACTIVATE_NOT_FOUND(options.id)}\n`); + return; + } + throw err; + } finally { + spinner.stop(); + } + + if (options.json) { + jsonOutput({ activated: true, id: options.id }); + return; + } + logSuccess(messages.FUNCTION_ACTIVATE_SUCCESS(options.id)); + }, +); diff --git a/src/commands/function/deactivate.ts b/src/commands/function/deactivate.ts new file mode 100644 index 0000000..34cd620 --- /dev/null +++ b/src/commands/function/deactivate.ts @@ -0,0 +1,38 @@ +import { logSuccess, logWarn } from '../../lib/logger'; +import { messages } from '../../lang/en'; +import { functionService } from '../../container'; +import { withCommandHandler } from '../../lib/command-handler'; +import { jsonOutput } from '../../lib/json-output'; +import { createSpinner } from '../../lib/ui'; +import { ApiError } from '../../lib/errors'; + +export const deactivateFunctionCommand = withCommandHandler( + async (options: { id: string; json?: boolean }): Promise => { + const spinner = createSpinner('Deactivating Brevo Function...', { silent: options.json }); + try { + await functionService.deactivateFunction(options.id); + } catch (err) { + if (err instanceof ApiError && err.statusCode === 404) { + spinner.stop(); + if (options.json) { + jsonOutput({ + error: 'not_found', + message: messages.FUNCTION_DEACTIVATE_NOT_FOUND(options.id), + }); + return; + } + logWarn(`\n ${messages.FUNCTION_DEACTIVATE_NOT_FOUND(options.id)}\n`); + return; + } + throw err; + } finally { + spinner.stop(); + } + + if (options.json) { + jsonOutput({ deactivated: true, id: options.id }); + return; + } + logSuccess(messages.FUNCTION_DEACTIVATE_SUCCESS(options.id)); + }, +); diff --git a/src/commands/function/delete.ts b/src/commands/function/delete.ts new file mode 100644 index 0000000..2229917 --- /dev/null +++ b/src/commands/function/delete.ts @@ -0,0 +1,54 @@ +import inquirer from 'inquirer'; +import { logSuccess, logInfo, logWarn } from '../../lib/logger'; +import { messages } from '../../lang/en'; +import { functionService } from '../../container'; +import { withCommandHandler } from '../../lib/command-handler'; +import { jsonOutput } from '../../lib/json-output'; +import { createSpinner } from '../../lib/ui'; +import { ApiError } from '../../lib/errors'; + +export const deleteFunctionCommand = withCommandHandler( + async (options: { id: string; force?: boolean; json?: boolean }): Promise => { + if (!options.force && !options.json) { + const { confirmed } = await inquirer.prompt([ + { + type: 'confirm', + name: 'confirmed', + message: messages.FUNCTION_DELETE_CONFIRM(options.id), + default: false, + }, + ]); + if (!confirmed) { + logInfo(`\n ${messages.FUNCTION_DELETE_CANCELLED}\n`); + return; + } + } + + const spinner = createSpinner('Deleting Brevo Function...', { silent: options.json }); + try { + await functionService.deleteFunction(options.id); + } catch (err) { + if (err instanceof ApiError && err.statusCode === 404) { + spinner.stop(); + if (options.json) { + jsonOutput({ + error: 'not_found', + message: messages.FUNCTION_DELETE_NOT_FOUND(options.id), + }); + return; + } + logWarn(`\n ${messages.FUNCTION_DELETE_NOT_FOUND(options.id)}\n`); + return; + } + throw err; + } finally { + spinner.stop(); + } + + if (options.json) { + jsonOutput({ deleted: true, id: options.id }); + return; + } + logSuccess(messages.FUNCTION_DELETE_SUCCESS(options.id)); + }, +); diff --git a/src/commands/preview-definitions.ts b/src/commands/preview-definitions.ts index 0a0a04e..4afeb4e 100644 --- a/src/commands/preview-definitions.ts +++ b/src/commands/preview-definitions.ts @@ -26,6 +26,9 @@ import { submitCommand } from './app/submit'; import { withdrawCommand } from './app/withdraw'; import { listFunctionCommand } from './function/list'; import { getFunctionCommand } from './function/get'; +import { activateFunctionCommand } from './function/activate'; +import { deactivateFunctionCommand } from './function/deactivate'; +import { deleteFunctionCommand } from './function/delete'; /** The `brevo app ` subcommands gated behind an unreleased feature. */ export const previewAppCommands: CommandDefinition[] = [ @@ -207,5 +210,43 @@ export const previewFunctionGroup: SubcommandGroupDefinition = { options: [{ flags: '--json', description: 'Output as JSON' }], handler: (opts, id) => getFunctionCommand({ id: id as string, json: Boolean(opts.json) }), }, + { + name: 'activate', + description: 'Activate a Brevo Function', + arguments: [{ name: '', description: 'Function ID' }], + examples: ['brevo function activate fn-001', 'brevo function activate fn-001 --json'], + options: [{ flags: '--json', description: 'Output as JSON' }], + handler: (opts, id) => + activateFunctionCommand({ id: id as string, json: Boolean(opts.json) }), + }, + { + name: 'deactivate', + description: 'Deactivate a Brevo Function', + arguments: [{ name: '', description: 'Function ID' }], + examples: ['brevo function deactivate fn-001', 'brevo function deactivate fn-001 --json'], + options: [{ flags: '--json', description: 'Output as JSON' }], + handler: (opts, id) => + deactivateFunctionCommand({ id: id as string, json: Boolean(opts.json) }), + }, + { + name: 'delete', + description: 'Delete a deployed Brevo Function', + arguments: [{ name: '', description: 'Function ID' }], + examples: [ + 'brevo function delete fn-001', + 'brevo function delete fn-001 --force', + 'brevo function delete fn-001 --json', + ], + options: [ + { flags: '--force', description: 'Skip confirmation' }, + { flags: '--json', description: 'Output as JSON' }, + ], + handler: (opts, id) => + deleteFunctionCommand({ + id: id as string, + force: Boolean(opts.force), + json: Boolean(opts.json), + }), + }, ], }; diff --git a/src/lang/preview-messages.ts b/src/lang/preview-messages.ts index 704b410..f7635a6 100644 --- a/src/lang/preview-messages.ts +++ b/src/lang/preview-messages.ts @@ -172,4 +172,19 @@ export const previewMessages = { APP_WITHDRAW_SUBMIT_HINT: (id: string) => `Submit it first: ${CLI.APP_SUBMIT(id)}`, APP_SUBMIT_NOT_PUBLIC: (appId: string): string => `App ${appId} is private. Private apps cannot be submitted for review. Only public apps are eligible for the approval process. Please make your app public before submitting it for review.`, + + // Function activate + FUNCTION_ACTIVATE_SUCCESS: (id: string) => `Brevo Function "${id}" activated.`, + FUNCTION_ACTIVATE_NOT_FOUND: (id: string) => `Brevo Function "${id}" not found.`, + + // Function deactivate + FUNCTION_DEACTIVATE_SUCCESS: (id: string) => `Brevo Function "${id}" deactivated.`, + FUNCTION_DEACTIVATE_NOT_FOUND: (id: string) => `Brevo Function "${id}" not found.`, + + // Function delete + FUNCTION_DELETE_CONFIRM: (id: string) => + `Are you sure you want to delete Brevo Function "${id}"? This cannot be undone.`, + FUNCTION_DELETE_SUCCESS: (id: string) => `Brevo Function "${id}" deleted.`, + FUNCTION_DELETE_CANCELLED: 'Deletion cancelled.', + FUNCTION_DELETE_NOT_FOUND: (id: string) => `Brevo Function "${id}" not found.`, } as const; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 1293d3c..ea008b2 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -192,6 +192,9 @@ export const CLI = { APP_SCOPES: 'brevo app available-scopes', FUNCTION_LIST: 'brevo function list', FUNCTION_GET: 'brevo function get', + FUNCTION_ACTIVATE: 'brevo function activate', + FUNCTION_DEACTIVATE: 'brevo function deactivate', + FUNCTION_DELETE: 'brevo function delete', SKILL_INSTALL: 'brevo skill:cli install', SKILL_UNINSTALL: 'brevo skill:cli uninstall', } as const; diff --git a/src/services/function.ts b/src/services/function.ts index 19cda7f..036a7a2 100644 --- a/src/services/function.ts +++ b/src/services/function.ts @@ -17,6 +17,18 @@ export function createFunctionService(client: ApiClient) { async fetchFunction(id: string): Promise { return client.get(ENDPOINTS.DP_FUNCTION(id)); }, + + async activateFunction(id: string): Promise { + await client.patch(ENDPOINTS.DP_FUNCTION(id), { is_active: true }); + }, + + async deactivateFunction(id: string): Promise { + await client.patch(ENDPOINTS.DP_FUNCTION(id), { is_active: false }); + }, + + async deleteFunction(id: string): Promise { + await client.delete(ENDPOINTS.DP_FUNCTION(id)); + }, }; }