From 48cd4009417aad188f24d7286acf02ada34bac06 Mon Sep 17 00:00:00 2001 From: Dennis Dornon Date: Thu, 26 Mar 2026 10:47:17 -0400 Subject: [PATCH 1/3] fix Windows tester feedback, add changelog, bump to 1.1.0-beta.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix Windows tester feedback (Git Bash as primary recommendation) - Fix debug redaction, ctrl-c exit code, CI audit - Fix test fixtures: site_id → site_id_or_domain - Show required params in abilities list examples - Add CHANGELOG.md with retroactive entries - Bump version to 1.1.0-beta.1 --- .github/workflows/ci.yml | 3 +- CHANGELOG.md | 62 +++++++++++++++++++ README.md | 20 +++--- docs/workflows/daily-health-check.md | 2 +- docs/workflows/monitoring-integration.md | 2 +- package-lock.json | 4 +- package.json | 2 +- src/__tests__/e2e/non-tty-behavior.test.ts | 2 +- src/__tests__/process/abilities-info.test.ts | 6 +- src/__tests__/process/abilities-list.test.ts | 11 ++++ src/__tests__/process/abilities-run.test.ts | 20 +++--- .../process/fixtures/api-responses.ts | 12 ++-- src/__tests__/process/safety.test.ts | 14 ++--- src/__tests__/process/scenarios.test.ts | 26 ++++---- src/commands/abilities/list.ts | 52 +++++++++++++++- src/commands/doctor.ts | 2 +- src/lib/base-command.ts | 2 +- src/utils/prompt.ts | 2 +- 18 files changed, 183 insertions(+), 61 deletions(-) create mode 100644 CHANGELOG.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be2e255..74a88e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,8 +20,7 @@ jobs: node-version: ${{ matrix.node }} cache: npm - run: npm ci - - run: npm audit --omit=dev - continue-on-error: true + - run: npm audit --omit=dev --audit-level=high - run: npm run lint - run: npm run typecheck - run: npm run build diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..dcfb20c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,62 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Required params shown in abilities list examples + +### Fixed + +- Debug redaction, ctrl-c exit code, CI audit +- Test fixtures: `site_id` → `site_id_or_domain` to match real API +- Windows tester feedback + +### Changed + +- Git Bash promoted as primary Windows recommendation + +## [1.0.1] - 2026-03-24 + +### Added + +- Windows/PowerShell guidance and skip links to docs + +## [1.0.0] - 2026-03-24 + +### Added + +- CLI binary (`mainwpcontrol`) with oclif command framework +- Commands: `login`, `profile list|use|delete`, `abilities list|info|run`, `jobs watch`, `doctor`, `chat`, `config show` +- Batch operations with polling and timeout +- Shell completion for bash and zsh +- Streaming chat with sliding window context management +- Audit logging for destructive actions +- Centralized secret masking utility +- Actionable recovery hints on error messages +- `--json` output flag and `defaultJsonOutput` setting +- Golden tests and E2E integration tests +- Profile validation in ProfileStore + +### Fixed + +- Keytar ESM/CJS interop +- Security vulnerabilities from audit (OWASP mapped) +- Exit code handling consistency + +### Changed + +- Binary renamed from `mainwpctl` to `mainwpcontrol` + +### Removed + +- Unimplemented `cancelJob` and `listJobs` from BatchManager + +[Unreleased]: https://github.com/mainwp/mainwp-control/compare/v1.0.1...HEAD +[1.0.1]: https://github.com/mainwp/mainwp-control/compare/v1.0.0...v1.0.1 +[1.0.0]: https://github.com/mainwp/mainwp-control/releases/tag/v1.0.0 diff --git a/README.md b/README.md index 63bab07..a086384 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A CLI for managing your MainWP Dashboard from the terminal. List sites, push upd ## Quick Start -> **On Windows?** Use [Git Bash](https://gitforwindows.org/) and every example below works without changes. +> **On Windows?** Use [Git Bash](https://gitforwindows.org/) and every example below works without changes. For scheduled workflows (cron), see [WSL](https://learn.microsoft.com/en-us/windows/wsl/install). You need Node.js 20+ and a MainWP Dashboard (v6+) with an [Application Password](https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/). @@ -65,10 +65,10 @@ mainwpcontrol abilities run list-updates-v1 --json **Get details for a specific site:** ```bash -mainwpcontrol abilities run get-site-v1 --input '{"site_id": 1}' --json +mainwpcontrol abilities run get-site-v1 --input '{"site_id_or_domain": 1}' --json ``` -> **Windows?** This works as-is in [Git Bash](https://gitforwindows.org/). In PowerShell, escape the inner quotes: `'{\"site_id\": 1}'`. Or skip quoting entirely with `--input-file` ([details](docs/workflows/input-from-file.md)). +> **Windows?** This works as-is in [Git Bash](https://gitforwindows.org/). In PowerShell, escape the inner quotes: `'{\"site_id_or_domain\": 1}'`. Or skip quoting entirely with `--input-file` ([details](docs/workflows/input-from-file.md)). **Preview a destructive action before running it:** @@ -175,10 +175,10 @@ When you pass JSON with `--input`, quoting depends on your shell: ```bash # macOS / Linux / Git Bash on Windows -mainwpcontrol abilities run get-site-v1 --input '{"site_id": 1}' --json +mainwpcontrol abilities run get-site-v1 --input '{"site_id_or_domain": 1}' --json # Windows PowerShell -mainwpcontrol abilities run get-site-v1 --input '{\"site_id\": 1}' --json +mainwpcontrol abilities run get-site-v1 --input '{\"site_id_or_domain\": 1}' --json ``` **Git Bash on Windows** (comes with [Git for Windows](https://gitforwindows.org/)) handles quoting the same way macOS and Linux do. If you use Git Bash, all the examples in this documentation work without changes. @@ -215,10 +215,10 @@ mainwpcontrol abilities info list-sites-v1 mainwpcontrol abilities run list-sites-v1 --json # Run with input parameters -mainwpcontrol abilities run get-site-v1 --input '{"site_id": 1}' --json +mainwpcontrol abilities run get-site-v1 --input '{"site_id_or_domain": 1}' --json # Windows PowerShell: escape inner quotes (Git Bash doesn't need this) -mainwpcontrol abilities run get-site-v1 --input '{\"site_id\": 1}' --json +mainwpcontrol abilities run get-site-v1 --input '{\"site_id_or_domain\": 1}' --json # Or use a file (works everywhere) mainwpcontrol abilities run get-site-v1 --input-file params.json --json @@ -401,11 +401,11 @@ fi mainwpcontrol abilities run update-site-plugins-v1 --input-file params.json --json # From stdin -echo '{"site_id": 1}' | mainwpcontrol abilities run get-site-v1 --input - --json +echo '{"site_id_or_domain": 1}' | mainwpcontrol abilities run get-site-v1 --input - --json # Heredoc mainwpcontrol abilities run get-site-v1 --input - --json < { json: false, quiet: false, debug: false, - input: '{"site_id": 1}', + input: '{"site_id_or_domain": 1}', 'dry-run': false, confirm: true, force: false, // No --force diff --git a/src/__tests__/process/abilities-info.test.ts b/src/__tests__/process/abilities-info.test.ts index f8331e9..3e3274e 100644 --- a/src/__tests__/process/abilities-info.test.ts +++ b/src/__tests__/process/abilities-info.test.ts @@ -179,10 +179,10 @@ describe('abilities info command', () => { expect(envelope.success).toBe(true); expect(envelope.data.name).toBe('mainwp/get-site-v1'); - // get-site-v1 has site_id as a required input parameter + // get-site-v1 has site_id_or_domain as a required input parameter expect(envelope.data.inputSchema.type).toBe('object'); - expect(envelope.data.inputSchema.properties).toHaveProperty('site_id'); - expect(envelope.data.inputSchema.required).toContain('site_id'); + expect(envelope.data.inputSchema.properties).toHaveProperty('site_id_or_domain'); + expect(envelope.data.inputSchema.required).toContain('site_id_or_domain'); }); // --------------------------------------------------------------------------- diff --git a/src/__tests__/process/abilities-list.test.ts b/src/__tests__/process/abilities-list.test.ts index 21c62a1..5f33cec 100644 --- a/src/__tests__/process/abilities-list.test.ts +++ b/src/__tests__/process/abilities-list.test.ts @@ -68,6 +68,17 @@ describe('abilities list command', () => { expect(output).toContain('run-updates-v1'); expect(output).toContain('list-updates-v1'); expect(output).toContain('get-site-plugins-v1'); + + // Abilities with required params show --input in their example + expect(output).toContain('abilities run get-site-v1 --input'); + expect(output).toContain('"site_id_or_domain"'); + + // Abilities without required params show bare command (no --input) + const listSitesLine = output.split('\n').find( + (l: string) => l.includes('abilities run list-sites-v1'), + ); + expect(listSitesLine).toBeDefined(); + expect(listSitesLine).not.toContain('--input'); }); // --------------------------------------------------------------------------- diff --git a/src/__tests__/process/abilities-run.test.ts b/src/__tests__/process/abilities-run.test.ts index f574666..433349d 100644 --- a/src/__tests__/process/abilities-run.test.ts +++ b/src/__tests__/process/abilities-run.test.ts @@ -108,7 +108,7 @@ describe('abilities run', () => { }); // ------------------------------------------------------------------------- - // 2. get-site-v1 --input '{"site_id": 5}' --json → GET with query params + // 2. get-site-v1 --input '{"site_id_or_domain": 5}' --json → GET with query params // ------------------------------------------------------------------------- describe('get-site-v1 --input (inline JSON)', () => { it('exits 0 and sends input as query params on a GET request', async () => { @@ -116,7 +116,7 @@ describe('abilities run', () => { const result = await run([ 'abilities', 'run', 'get-site-v1', - '--input', '{"site_id": 5}', + '--input', '{"site_id_or_domain": 5}', '--json', ]); @@ -129,7 +129,7 @@ describe('abilities run', () => { const req = server.getLastRequest('/run'); expect(req).toBeDefined(); expect(req!.method).toBe('GET'); - expect(req!.query['input[site_id]']).toBe('5'); + expect(req!.query['input[site_id_or_domain]']).toBe('5'); }); }); @@ -148,7 +148,7 @@ describe('abilities run', () => { }); it('reads input from the specified file and sends it as query params', async () => { - await writeFile(tmpFile, JSON.stringify({ site_id: 5 }), 'utf-8'); + await writeFile(tmpFile, JSON.stringify({ site_id_or_domain: 5 }), 'utf-8'); server.setRunResponse('get-site-v1', abilityRunSuccess(singleSitePayload)); const result = await run([ @@ -162,16 +162,16 @@ describe('abilities run', () => { const envelope = result.json as { success: boolean; data: Record }; expect(envelope.success).toBe(true); - // Verify the server received site_id in query params (GET for readonly) + // Verify the server received site_id_or_domain in query params (GET for readonly) const req = server.getLastRequest('/run'); expect(req).toBeDefined(); expect(req!.method).toBe('GET'); - expect(req!.query['input[site_id]']).toBe('5'); + expect(req!.query['input[site_id_or_domain]']).toBe('5'); }); }); // ------------------------------------------------------------------------- - // 4. echo '{"site_id": 5}' | mainwpcontrol abilities run get-site-v1 --input - --json + // 4. echo '{"site_id_or_domain": 5}' | mainwpcontrol abilities run get-site-v1 --input - --json // ------------------------------------------------------------------------- describe('get-site-v1 --input - (stdin pipe)', () => { it('reads input from stdin and sends it as query params', async () => { @@ -179,7 +179,7 @@ describe('abilities run', () => { const result = await run( ['abilities', 'run', 'get-site-v1', '--input', '-', '--json'], - { stdin: '{"site_id": 5}' }, + { stdin: '{"site_id_or_domain": 5}' }, ); expect(result.exitCode).toBe(0); @@ -187,11 +187,11 @@ describe('abilities run', () => { const envelope = result.json as { success: boolean; data: Record }; expect(envelope.success).toBe(true); - // Verify the server received site_id in query params (GET for readonly) + // Verify the server received site_id_or_domain in query params (GET for readonly) const req = server.getLastRequest('/run'); expect(req).toBeDefined(); expect(req!.method).toBe('GET'); - expect(req!.query['input[site_id]']).toBe('5'); + expect(req!.query['input[site_id_or_domain]']).toBe('5'); }); }); diff --git a/src/__tests__/process/fixtures/api-responses.ts b/src/__tests__/process/fixtures/api-responses.ts index 0057d6a..1e1e7d3 100644 --- a/src/__tests__/process/fixtures/api-responses.ts +++ b/src/__tests__/process/fixtures/api-responses.ts @@ -100,8 +100,8 @@ export const STANDARD_ABILITIES = [ category: 'sites', input_schema: { type: 'object', - properties: { site_id: { type: 'integer', description: 'Site ID' } }, - required: ['site_id'], + properties: { site_id_or_domain: { type: 'integer', description: 'Site ID or domain' } }, + required: ['site_id_or_domain'], }, }), mockAbility({ @@ -110,8 +110,8 @@ export const STANDARD_ABILITIES = [ category: 'sites', input_schema: { type: 'object', - properties: { site_id: { type: 'integer', description: 'Site ID' } }, - required: ['site_id'], + properties: { site_id_or_domain: { type: 'integer', description: 'Site ID or domain' } }, + required: ['site_id_or_domain'], }, }), mockAbility({ name: 'mainwp/sync-sites-v1', category: 'sites' }), @@ -123,8 +123,8 @@ export const STANDARD_ABILITIES = [ category: 'plugins', input_schema: { type: 'object', - properties: { site_id: { type: 'integer', description: 'Site ID' } }, - required: ['site_id'], + properties: { site_id_or_domain: { type: 'integer', description: 'Site ID or domain' } }, + required: ['site_id_or_domain'], }, }), mockAbility({ diff --git a/src/__tests__/process/safety.test.ts b/src/__tests__/process/safety.test.ts index 70dbcbc..d835636 100644 --- a/src/__tests__/process/safety.test.ts +++ b/src/__tests__/process/safety.test.ts @@ -55,7 +55,7 @@ describe('safety / destructive action handling', () => { ); const result = await runCLI( - ['abilities', 'run', 'delete-site-v1', '--input', '{"site_id":1}', '--dry-run', '--json'], + ['abilities', 'run', 'delete-site-v1', '--input', '{"site_id_or_domain":1}', '--dry-run', '--json'], { xdgConfigHome: config.xdgHome, env: { MAINWP_APP_PASSWORD: 'test-pass' }, @@ -78,7 +78,7 @@ describe('safety / destructive action handling', () => { const body = runReq.body as Record; const input = body['input'] as Record; expect(input).toHaveProperty('dry_run', true); - expect(input).toHaveProperty('site_id', 1); + expect(input).toHaveProperty('site_id_or_domain', 1); // confirm must NOT be present expect(input).not.toHaveProperty('confirm'); @@ -121,7 +121,7 @@ describe('safety / destructive action handling', () => { const result = await runCLI( [ 'abilities', 'run', 'delete-site-v1', - '--input', '{"site_id":1}', + '--input', '{"site_id_or_domain":1}', '--confirm', '--force', '--json', ], { @@ -152,7 +152,7 @@ describe('safety / destructive action handling', () => { const confirmInput = (confirmReq!.body as Record)['input'] as Record; expect(confirmInput).toHaveProperty('confirm', true); expect(confirmInput).toHaveProperty('user_confirmed', true); - expect(confirmInput).toHaveProperty('site_id', 1); + expect(confirmInput).toHaveProperty('site_id_or_domain', 1); // The dry_run request should also be present const dryRunReq = runRequests.find((r) => { @@ -180,7 +180,7 @@ describe('safety / destructive action handling', () => { const result = await runCLI( [ 'abilities', 'run', 'delete-site-v1', - '--input', '{"site_id":1}', + '--input', '{"site_id_or_domain":1}', '--dry-run', '--confirm', ], { @@ -213,7 +213,7 @@ describe('safety / destructive action handling', () => { const result = await runCLI( [ 'abilities', 'run', 'delete-site-v1', - '--input', '{"site_id":1}', + '--input', '{"site_id_or_domain":1}', '--json', ], { @@ -250,7 +250,7 @@ describe('safety / destructive action handling', () => { const result = await runCLI( [ 'abilities', 'run', 'delete-site-v1', - '--input', '{"site_id":1}', + '--input', '{"site_id_or_domain":1}', '--confirm', '--json', ], { diff --git a/src/__tests__/process/scenarios.test.ts b/src/__tests__/process/scenarios.test.ts index 8365233..f1d80c8 100644 --- a/src/__tests__/process/scenarios.test.ts +++ b/src/__tests__/process/scenarios.test.ts @@ -448,7 +448,7 @@ describe('Scenario: Plugin Deployment Verification', () => { const result = await runWithProfile( [ 'abilities', 'run', 'get-site-plugins-v1', - '--input', '{"site_id": 1}', + '--input', '{"site_id_or_domain": 1}', '--json', ], config, @@ -491,14 +491,14 @@ describe('Scenario: Plugin Deployment Verification', () => { expect(plugins[2]).toMatchObject({ name: 'woocommerce', version: '8.5', active: false }); }); - it('step 4: site_id is sent as query param (GET for readonly)', async () => { + it('site_id_or_domain is sent as query param (GET for readonly)', async () => { await loginCLI(server, config); server.setRunResponse('get-site-plugins-v1', abilityRunSuccess(site1Plugins)); await runWithProfile( [ 'abilities', 'run', 'get-site-plugins-v1', - '--input', '{"site_id": 1}', + '--input', '{"site_id_or_domain": 1}', '--json', ], config, @@ -508,7 +508,7 @@ describe('Scenario: Plugin Deployment Verification', () => { const req = server.getLastRequest('/run'); expect(req).toBeDefined(); expect(req!.method).toBe('GET'); - expect(req!.query['input[site_id]']).toBe('1'); + expect(req!.query['input[site_id_or_domain]']).toBe('1'); }); it('full flow: login → list sites → get plugins for first site', async () => { @@ -537,7 +537,7 @@ describe('Scenario: Plugin Deployment Verification', () => { const pluginsResult = await runWithProfile( [ 'abilities', 'run', 'get-site-plugins-v1', - '--input', JSON.stringify({ site_id: firstSiteId }), + '--input', JSON.stringify({ site_id_or_domain: firstSiteId }), '--json', ], config, @@ -613,7 +613,7 @@ describe('Scenario: Input Methods Equivalence', () => { const result = await runWithProfile( [ 'abilities', 'run', 'get-site-v1', - '--input', '{"site_id": 5}', + '--input', '{"site_id_or_domain": 5}', '--json', ], config, @@ -632,7 +632,7 @@ describe('Scenario: Input Methods Equivalence', () => { }); it('method 2: --input-file exits 0 with correct data', async () => { - await writeFile(tmpFilePath, JSON.stringify({ site_id: 5 }), 'utf-8'); + await writeFile(tmpFilePath, JSON.stringify({ site_id_or_domain: 5 }), 'utf-8'); server.setRunResponse('get-site-v1', abilityRunSuccess(singleSitePayload)); const result = await runWithProfile( @@ -666,7 +666,7 @@ describe('Scenario: Input Methods Equivalence', () => { '--json', ], config, - { stdin: '{"site_id": 5}' }, + { stdin: '{"site_id_or_domain": 5}' }, ); expect(result.exitCode).toBe(0); @@ -682,7 +682,7 @@ describe('Scenario: Input Methods Equivalence', () => { }); it('all 3 methods produce equivalent server-side params', async () => { - const inputJson = '{"site_id": 5}'; + const inputJson = '{"site_id_or_domain": 5}'; const results: Array<{ method: string; query: Record; response: unknown }> = []; // Method 1: --input inline @@ -730,9 +730,9 @@ describe('Scenario: Input Methods Equivalence', () => { }); // All three should have sent the same query params to the server - expect(results[0]!.query['input[site_id]']).toBe('5'); - expect(results[1]!.query['input[site_id]']).toBe('5'); - expect(results[2]!.query['input[site_id]']).toBe('5'); + expect(results[0]!.query['input[site_id_or_domain]']).toBe('5'); + expect(results[1]!.query['input[site_id_or_domain]']).toBe('5'); + expect(results[2]!.query['input[site_id_or_domain]']).toBe('5'); // All three query objects should be equivalent expect(results[0]!.query).toEqual(results[1]!.query); @@ -747,7 +747,7 @@ describe('Scenario: Input Methods Equivalence', () => { }); it('all 3 methods use GET for readonly ability', async () => { - const inputJson = '{"site_id": 5}'; + const inputJson = '{"site_id_or_domain": 5}'; const methods: string[] = []; // Inline diff --git a/src/commands/abilities/list.ts b/src/commands/abilities/list.ts index bc8e106..2ea4b68 100644 --- a/src/commands/abilities/list.ts +++ b/src/commands/abilities/list.ts @@ -9,6 +9,55 @@ import { BaseCommand, commonFlags } from '../../lib/base-command.js'; import { formatTable, formatHeading } from '../../output/formatter.js'; import { color, colors } from '../../utils/colors.js'; import { stripControlChars } from '../../utils/terminal-sanitizer.js'; +import type { Ability } from '../../core/abilities-executor.js'; + +/** + * Build a copy-pasteable usage hint for an ability. + * If the ability has required parameters, includes --input with placeholder values. + */ +function buildUsageHint(shortName: string, ability: Ability): string { + const schema = ability.input_schema as + | { required?: string[]; properties?: Record } + | undefined; + const required = schema?.required; + if (!required || required.length === 0) { + return `mainwpcontrol abilities run ${shortName}`; + } + + const properties = schema?.properties ?? {}; + const params: Record = {}; + + for (const paramName of required) { + params[paramName] = placeholderFor(paramName, properties[paramName]?.type); + } + + return `mainwpcontrol abilities run ${shortName} --input '${JSON.stringify(params)}'`; +} + +function placeholderFor(name: string, type?: string): unknown { + // Specific names first + if (name === 'job_id') return 'sync_abc123'; + if (name === 'url') return 'https://example.com'; + if (name === 'admin_username') return 'admin'; + if (name === 'name') return 'My Name'; + if (name === 'action') return 'ignore'; + if (name === 'type') return 'plugin'; + if (name === 'slug') return 'akismet/akismet.php'; + if (name === 'theme') return 'theme-slug'; + if (name === 'plugins') return ['akismet/akismet.php']; + if (name === 'themes') return ['theme-slug']; + if (name === 'slugs') return ['slug']; + + // Pattern-based names + if (name.endsWith('_id_or_email') || name.endsWith('_id_or_domain')) return 1; + if (name.endsWith('_id')) return 1; + + // Fall back to schema type + if (type === 'integer' || type === 'number') return 1; + if (type === 'boolean') return true; + if (type === 'array') return ['value']; + return 'value'; +} export default class AbilitiesList extends BaseCommand { static description = 'List available abilities'; @@ -107,7 +156,8 @@ export default class AbilitiesList extends BaseCommand { const ability = catAbilities[j]!; const safeName = stripControlChars(ability.name); const shortName = safeName.split('/').pop() ?? safeName; - lines.push(color(` mainwpcontrol abilities run ${shortName}`, colors.dim)); + const hint = buildUsageHint(shortName, ability); + lines.push(color(` ${hint}`, colors.dim)); } lines.push(''); diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index 239e534..83ab561 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -333,7 +333,7 @@ export default class DoctorCommand extends BaseCommand { }; } - const requiredAbilities = ['list-sites-v1', 'get-site-v1']; + const requiredAbilities = ['mainwp/list-sites-v1', 'mainwp/get-site-v1']; const missing = requiredAbilities.filter( (name) => !abilities.find((a) => a.name === name) ); diff --git a/src/lib/base-command.ts b/src/lib/base-command.ts index fc9373c..3a9a63e 100644 --- a/src/lib/base-command.ts +++ b/src/lib/base-command.ts @@ -296,7 +296,7 @@ export abstract class BaseCommand extends Command { const redacted: Record = {}; for (const [key, value] of Object.entries(context)) { - if (sensitiveKeys.includes(key.toLowerCase())) { + if (sensitiveKeys.some(s => key.toLowerCase().includes(s))) { redacted[key] = '[REDACTED]'; continue; } diff --git a/src/utils/prompt.ts b/src/utils/prompt.ts index 46048c7..e42e91d 100644 --- a/src/utils/prompt.ts +++ b/src/utils/prompt.ts @@ -136,7 +136,7 @@ export async function promptForPassword(question: string): Promise { stdin.removeListener('data', onData); rl.close(); process.stdout.write('\n'); - process.exit(1); + process.exit(130); break; case '\u007F': // Backspace From cb3b6871d5f8ee1a3ce4e797a58a338fcd1722d9 Mon Sep 17 00:00:00 2001 From: Dennis Dornon Date: Thu, 26 Mar 2026 11:09:04 -0400 Subject: [PATCH 2/3] fix npm audit (picomatch) and debug redaction false positive - bump picomatch to fix high-severity ReDoS - use exact key match for redaction so passwordSource isn't redacted --- package-lock.json | 16 ++++++++-------- src/lib/base-command.ts | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 03f403b..7be7c58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@mainwp/control", - "version": "1.0.2", + "version": "1.1.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@mainwp/control", - "version": "1.0.2", + "version": "1.1.0-beta.1", "license": "GPL-3.0-or-later", "dependencies": { "@oclif/core": "~4.0.0", @@ -7179,9 +7179,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -8081,9 +8081,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { diff --git a/src/lib/base-command.ts b/src/lib/base-command.ts index 3a9a63e..580a9e3 100644 --- a/src/lib/base-command.ts +++ b/src/lib/base-command.ts @@ -296,7 +296,7 @@ export abstract class BaseCommand extends Command { const redacted: Record = {}; for (const [key, value] of Object.entries(context)) { - if (sensitiveKeys.some(s => key.toLowerCase().includes(s))) { + if (sensitiveKeys.some(s => key.toLowerCase() === s)) { redacted[key] = '[REDACTED]'; continue; } From 661964590d29b9365c6061398c10895e45433c2a Mon Sep 17 00:00:00 2001 From: Dennis Dornon Date: Thu, 26 Mar 2026 11:12:45 -0400 Subject: [PATCH 3/3] move changelog entries from Unreleased to 1.1.0-beta.1 --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcfb20c..f41ca11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.1.0-beta.1] - 2026-03-26 + ### Added - Required params shown in abilities list examples @@ -57,6 +59,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Unimplemented `cancelJob` and `listJobs` from BatchManager -[Unreleased]: https://github.com/mainwp/mainwp-control/compare/v1.0.1...HEAD +[Unreleased]: https://github.com/mainwp/mainwp-control/compare/v1.1.0-beta.1...HEAD +[1.1.0-beta.1]: https://github.com/mainwp/mainwp-control/compare/v1.0.1...v1.1.0-beta.1 [1.0.1]: https://github.com/mainwp/mainwp-control/compare/v1.0.0...v1.0.1 [1.0.0]: https://github.com/mainwp/mainwp-control/releases/tag/v1.0.0