From 1636bd447e2fd53f80abf443bab3e3295dd481ba Mon Sep 17 00:00:00 2001 From: "heecheol.park" Date: Sat, 20 Jun 2026 21:28:52 +0900 Subject: [PATCH 1/2] feat: add global --json flag and deprecate per-command --format json Introduce a global --json flag as the canonical way to request machine-readable output, and deprecate the per-command --format json (still honored, warns once on stderr). - Add JSON output to search, spaces, and find (previously unavailable) - Route existing --format json commands through a shared lib/output helper (info, children, versions, comments, attachments, property-*) - Reject --json on commands that don't support it with a clear error instead of silently emitting human-readable output - Fix comments emitting "No comments found." to stdout in JSON mode Refs #203 --- README.md | 42 ++++++---- bin/commands/attachments.js | 19 +++-- bin/commands/comment.js | 20 +++-- bin/commands/properties.js | 24 +++--- bin/commands/versions.js | 8 +- bin/confluence.js | 80 ++++++++++++++---- lib/output.js | 36 ++++++++ tests/json-output.test.js | 160 ++++++++++++++++++++++++++++++++++++ 8 files changed, 328 insertions(+), 61 deletions(-) create mode 100644 lib/output.js create mode 100644 tests/json-output.test.js diff --git a/README.md b/README.md index cebbca2..a657377 100644 --- a/README.md +++ b/README.md @@ -341,6 +341,20 @@ For **read-only** usage, select at minimum: `read:confluence-content.all`, `read ## Usage +### JSON output (for scripting / jq) + +Pass the global `--json` flag to make any read command emit raw JSON to stdout, so you can pipe it to tools like `jq`: + +```bash +confluence search "release notes" --json | jq '.results[].title' +confluence spaces --json | jq -r '.spaces[].key' +confluence info 123456789 --json | jq '.version' +``` + +`--json` works on `info`, `search`, `spaces`, `find`, `children`, `versions`, `comments`, `attachments`, `property-list`, `property-get`, and `property-set`. Passing `--json` to any other command is rejected with an error rather than silently ignored. Human-readable messages and warnings go to stderr, so stdout stays valid JSON. + +> **Deprecation:** the per-command `--format json` form is deprecated in favor of the global `--json` flag. It still works but prints a warning to stderr and will be removed in a future major version. + ### Read a Page ```bash # Read by page ID @@ -363,7 +377,7 @@ Use `--format storage` when you need Confluence's native storage representation, confluence info 123456789 # Emit machine-readable metadata -confluence info 123456789 --format json +confluence info 123456789 --json ``` Example JSON shape: @@ -515,10 +529,10 @@ confluence children 123456789 --show-id --show-url confluence children 123456789 --recursive --max-depth 3 # Output as JSON for scripting -confluence children 123456789 --recursive --format json > children.json +confluence children 123456789 --recursive --json > children.json ``` -`children --format json` returns structured metadata for each page, including `id`, `title`, `type`, `status`, `spaceKey`, `parentId`, `version`, and `url`. Recursive output also includes `depth`, and when available, `ancestors`. +`children --json` returns structured metadata for each page, including `id`, `title`, `type`, `status`, `spaceKey`, `parentId`, `version`, and `url`. Recursive output also includes `depth`, and when available, `ancestors`. Example recursive JSON item: ```json @@ -773,30 +787,30 @@ confluence stats |---|---|---| | `init` | Initialize CLI configuration | `--read-only` | | `read ` | Read page content | `--format ` | -| `info ` | Get page information | `--format ` | -| `search ` | Search for pages | `--limit `, `--start ` | -| `spaces` | List available spaces | `--limit `, `--all` | -| `find ` | Find a page by its title | `--space <spaceKey>` | -| `children <pageId>` | List child pages of a page | `--recursive`, `--max-depth <number>`, `--format <list\|tree\|json>`, `--show-url`, `--show-id` | +| `info <pageId_or_url>` | Get page information | `--json` | +| `search <query>` | Search for pages | `--json`, `--limit <number>`, `--start <number>` | +| `spaces` | List available spaces | `--json`, `--limit <number>`, `--all` | +| `find <title>` | Find a page by its title | `--space <spaceKey>`, `--json` | +| `children <pageId>` | List child pages of a page | `--recursive`, `--max-depth <number>`, `--format <list\|tree>`, `--json`, `--show-url`, `--show-id` | | `create <title> <spaceKey>` | Create a new page or folder | `--content <string>`, `--file <path>`, `--format <auto\|storage\|html\|markdown>`, `--type <page\|folder>` | | `create-child <title> <parentId>` | Create a child page or folder | `--content <string>`, `--file <path>`, `--format <auto\|storage\|html\|markdown>`, `--type <page\|folder>` | | `copy-tree <sourcePageId> <targetParentId> [newTitle]` | Copy page tree with all children | `--max-depth <number>`, `--exclude <patterns>`, `--delay-ms <ms>`, `--copy-suffix <text>`, `--dry-run`, `--fail-on-error`, `--quiet` | | `update <pageId>` | Update a page's title or content | `--title <string>`, `--content <string>`, `--file <path>`, `--format <auto\|storage\|html\|markdown>` | | `move <pageId_or_url> <newParentId_or_url>` | Move a page to a new parent location | `--title <string>` | | `delete <pageId_or_url>` | Delete a page by ID or URL | `--yes` | -| `versions <pageId_or_url>` | List historical versions of a page | `--format <text\|json>` | +| `versions <pageId_or_url>` | List historical versions of a page | `--json` | | `version-delete <pageId_or_url> <versionNumber>` | Delete a single non-current version of a page | `--yes` | | `versions-purge <pageId_or_url>` | Delete every non-current historical version of a page | `--yes`, `--throttle <seconds>` | | `edit <pageId>` | Export page content for editing | `--output <file>` | -| `attachments <pageId_or_url>` | List or download attachments for a page | `--limit <number>`, `--pattern <glob>`, `--download`, `--dest <directory>` | +| `attachments <pageId_or_url>` | List or download attachments for a page | `--json`, `--limit <number>`, `--pattern <glob>`, `--download`, `--dest <directory>` | | `attachment-upload <pageId_or_url>` | Upload attachments to a page | `--file <path>`, `--comment <text>`, `--replace`, `--minor-edit` | | `attachment-delete <pageId_or_url> <attachmentId>` | Delete an attachment from a page | `--yes` | -| `comments <pageId_or_url>` | List comments for a page | `--format <text\|markdown\|json>`, `--limit <number>`, `--start <number>`, `--location <inline\|footer\|resolved>`, `--depth <root\|all>`, `--all` | +| `comments <pageId_or_url>` | List comments for a page | `--format <text\|markdown>`, `--json`, `--limit <number>`, `--start <number>`, `--location <inline\|footer\|resolved>`, `--depth <root\|all>`, `--all` | | `comment <pageId_or_url>` | Create a comment on a page | `--content <string>`, `--file <path>`, `--format <auto\|storage\|html\|markdown>`, `--parent <commentId>`, `--location <inline\|footer>`, `--inline-selection <text>`, `--inline-original-selection <text>`, `--inline-marker-ref <ref>`, `--inline-properties <json>` | | `comment-delete <commentId>` | Delete a comment by ID | `--yes` | -| `property-list <pageId_or_url>` | List all content properties for a page | `--format <text\|json>`, `--limit <number>`, `--start <number>`, `--all` | -| `property-get <pageId_or_url> <key>` | Get a content property by key | `--format <text\|json>` | -| `property-set <pageId_or_url> <key>` | Set a content property (create or update) | `--value <json>`, `--file <path>`, `--format <text\|json>` | +| `property-list <pageId_or_url>` | List all content properties for a page | `--json`, `--limit <number>`, `--start <number>`, `--all` | +| `property-get <pageId_or_url> <key>` | Get a content property by key | `--json` | +| `property-set <pageId_or_url> <key>` | Set a content property (create or update) | `--value <json>`, `--file <path>`, `--json` | | `property-delete <pageId_or_url> <key>` | Delete a content property by key | `--yes` | | `export <pageId_or_url>` | Export a page to a directory with its attachments | `--format <html\|text\|markdown>`, `--dest <directory>`, `--file <filename>`, `--attachments-dir <name>`, `--pattern <glob>`, `--referenced-only`, `--skip-attachments` | | `profile list` | List all configuration profiles | | diff --git a/bin/commands/attachments.js b/bin/commands/attachments.js index 6a1ebe1..69f0183 100644 --- a/bin/commands/attachments.js +++ b/bin/commands/attachments.js @@ -12,8 +12,8 @@ function registerAttachmentCommands(program, { withClient }) { .option('-p, --pattern <glob>', 'Filter attachments by filename (e.g., "*.png")') .option('-d, --download', 'Download matching attachments') .option('--dest <directory>', 'Directory to save downloads (default: current directory)', '.') - .option('-f, --format <format>', 'Output format (text, json)', 'text') - .action(withClient('attachments', async ({ client, analytics }, pageId, options) => { + .option('-f, --format <format>', 'Output format (text). "json" is deprecated — use --json', 'text') + .action(withClient('attachments', async ({ client, analytics, wantsJson, emitJson }, pageId, options) => { const maxResults = options.limit ? parseInt(options.limit, 10) : null; const pattern = options.pattern ? options.pattern.trim() : null; @@ -25,13 +25,14 @@ function registerAttachmentCommands(program, { withClient }) { if (!['text', 'json'].includes(format)) { throw new Error('Format must be one of: text, json'); } + const jsonMode = wantsJson(options); const attachments = await client.getAllAttachments(pageId, { maxResults }); const filtered = pattern ? attachments.filter(att => client.matchesPattern(att.title, pattern)) : attachments; if (filtered.length === 0) { - if (format === 'json') { - console.log(JSON.stringify({ attachmentCount: 0, attachments: [] }, null, 2)); + if (jsonMode) { + emitJson({ attachmentCount: 0, attachments: [] }); } else { console.log(chalk.yellow('No attachments found.')); } @@ -39,7 +40,7 @@ function registerAttachmentCommands(program, { withClient }) { return; } - if (format === 'json' && !options.download) { + if (jsonMode && !options.download) { const output = { attachmentCount: filtered.length, attachments: filtered.map(att => ({ @@ -52,7 +53,7 @@ function registerAttachmentCommands(program, { withClient }) { downloadLink: att.downloadLink, })), }; - console.log(JSON.stringify(output, null, 2)); + emitJson(output); } else if (!options.download) { console.log(chalk.blue(`Found ${filtered.length} attachment${filtered.length === 1 ? '' : 's'}:`)); filtered.forEach((att, index) => { @@ -95,19 +96,19 @@ function registerAttachmentCommands(program, { withClient }) { const dataStream = await client.downloadAttachment(pageId, attachment); await writeStream(dataStream, targetPath); downloadResults.push({ title: attachment.title, id: attachment.id, savedTo: targetPath }); - if (format !== 'json') { + if (!jsonMode) { console.log(`⬇️ ${chalk.green(attachment.title)} -> ${chalk.gray(targetPath)}`); } } - if (format === 'json') { + if (jsonMode) { const output = { attachmentCount: filtered.length, downloaded: downloadResults.length, destination: destDir, attachments: downloadResults, }; - console.log(JSON.stringify(output, null, 2)); + emitJson(output); } else { console.log(chalk.green(`Downloaded ${downloadResults.length} attachment${downloadResults.length === 1 ? '' : 's'} to ${destDir}`)); } diff --git a/bin/commands/comment.js b/bin/commands/comment.js index 0169229..328d0f0 100644 --- a/bin/commands/comment.js +++ b/bin/commands/comment.js @@ -49,17 +49,18 @@ function registerCommentCommands(program, { withClient }) { program .command('comments <pageId>') .description('List comments for a page by ID or URL') - .option('-f, --format <format>', 'Output format (text, markdown, json)', 'text') + .option('-f, --format <format>', 'Output format (text, markdown). "json" is deprecated — use --json', 'text') .option('-l, --limit <limit>', 'Maximum number of comments to fetch (default: 25)') .option('--start <start>', 'Start index for results (default: 0)', '0') .option('--location <location>', 'Filter by location (inline, footer, resolved). Comma-separated') .option('--depth <depth>', 'Comment depth ("" for root only, "all")') .option('--all', 'Fetch all comments (ignores pagination)') - .action(withClient('comments', async ({ client, analytics }, pageId, options) => { + .action(withClient('comments', async ({ client, analytics, wantsJson, emitJson }, pageId, options) => { const format = (options.format || 'text').toLowerCase(); if (!['text', 'markdown', 'json'].includes(format)) { throw new Error('Format must be one of: text, markdown, json'); } + const jsonMode = wantsJson(options); const limit = options.limit ? parseInt(options.limit, 10) : null; if (options.limit && (Number.isNaN(limit) || limit <= 0)) { @@ -102,12 +103,21 @@ function registerCommentCommands(program, { withClient }) { } if (comments.length === 0) { - console.log(chalk.yellow('No comments found.')); + if (jsonMode) { + const resolvedPageId = await client.extractPageId(pageId); + const output = { pageId: resolvedPageId, commentCount: 0, comments: [] }; + if (!options.all) { + output.nextStart = nextStart; + } + emitJson(output); + } else { + console.log(chalk.yellow('No comments found.')); + } analytics.track('comments', true); return; } - if (format === 'json') { + if (jsonMode) { const resolvedPageId = await client.extractPageId(pageId); const output = { pageId: resolvedPageId, @@ -121,7 +131,7 @@ function registerCommentCommands(program, { withClient }) { if (!options.all) { output.nextStart = nextStart; } - console.log(JSON.stringify(output, null, 2)); + emitJson(output); analytics.track('comments', true); return; } diff --git a/bin/commands/properties.js b/bin/commands/properties.js index 3f494a7..80f034d 100644 --- a/bin/commands/properties.js +++ b/bin/commands/properties.js @@ -6,11 +6,11 @@ function registerPropertyCommands(program, { withClient }) { program .command('property-list <pageId>') .description('List all content properties for a page') - .option('-f, --format <format>', 'Output format (text, json)', 'text') + .option('-f, --format <format>', 'Output format (text). "json" is deprecated — use --json', 'text') .option('-l, --limit <limit>', 'Maximum number of properties to fetch (default: 25)') .option('--start <start>', 'Start index for results (default: 0)', '0') .option('--all', 'Fetch all properties (ignores pagination)') - .action(withClient('property_list', async ({ client, analytics }, pageId, options) => { + .action(withClient('property_list', async ({ client, analytics, wantsJson, emitJson }, pageId, options) => { const format = (options.format || 'text').toLowerCase(); if (!['text', 'json'].includes(format)) { throw new Error('Format must be one of: text, json'); @@ -43,12 +43,12 @@ function registerPropertyCommands(program, { withClient }) { nextStart = response.nextStart; } - if (format === 'json') { + if (wantsJson(options)) { const output = { properties }; if (!options.all) { output.nextStart = nextStart; } - console.log(JSON.stringify(output, null, 2)); + emitJson(output); } else if (properties.length === 0) { console.log(chalk.yellow('No properties found.')); } else { @@ -68,8 +68,8 @@ function registerPropertyCommands(program, { withClient }) { program .command('property-get <pageId> <key>') .description('Get a content property by key') - .option('-f, --format <format>', 'Output format (text, json)', 'text') - .action(withClient('property_get', async ({ client, analytics }, pageId, key, options) => { + .option('-f, --format <format>', 'Output format (text). "json" is deprecated — use --json', 'text') + .action(withClient('property_get', async ({ client, analytics, wantsJson, emitJson }, pageId, key, options) => { const format = (options.format || 'text').toLowerCase(); if (!['text', 'json'].includes(format)) { throw new Error('Format must be one of: text, json'); @@ -77,8 +77,8 @@ function registerPropertyCommands(program, { withClient }) { const property = await client.getProperty(pageId, key); - if (format === 'json') { - console.log(JSON.stringify(property, null, 2)); + if (wantsJson(options)) { + emitJson(property); } else { console.log(`${chalk.green('Key:')} ${property.key}`); console.log(`${chalk.green('Version:')} ${property.version.number}`); @@ -93,8 +93,8 @@ function registerPropertyCommands(program, { withClient }) { .description('Set a content property (create or update)') .option('-v, --value <json>', 'Property value as JSON') .option('--file <file>', 'Read property value from a JSON file') - .option('-f, --format <format>', 'Output format (text, json)', 'text') - .action(withClient('property_set', async ({ client, analytics }, pageId, key, options) => { + .option('-f, --format <format>', 'Output format (text). "json" is deprecated — use --json', 'text') + .action(withClient('property_set', async ({ client, analytics, wantsJson, emitJson }, pageId, key, options) => { if (!options.value && !options.file) { throw new Error('Provide a value with --value or --file.'); } @@ -122,8 +122,8 @@ function registerPropertyCommands(program, { withClient }) { const result = await client.setProperty(pageId, key, value); - if (format === 'json') { - console.log(JSON.stringify(result, null, 2)); + if (wantsJson(options)) { + emitJson(result); } else { console.log(chalk.green('✅ Property set successfully!')); console.log(`${chalk.green('Key:')} ${result.key}`); diff --git a/bin/commands/versions.js b/bin/commands/versions.js index 50b5b5b..8201e5a 100644 --- a/bin/commands/versions.js +++ b/bin/commands/versions.js @@ -5,13 +5,13 @@ function registerVersionCommands(program, { withClient }) { program .command('versions <pageId>') .description('List historical versions of a Confluence page') - .option('--format <format>', 'Output format: text or json (default: text)', 'text') - .action(withClient('versions', async ({ client, analytics }, pageId, options) => { + .option('--format <format>', 'Output format: text (default). "json" is deprecated — use --json', 'text') + .action(withClient('versions', async ({ client, analytics, wantsJson, emitJson }, pageId, options) => { const resolvedId = String(await client.extractPageId(pageId)); const versions = await client.listVersions(resolvedId); - if (options.format === 'json') { - console.log(JSON.stringify({ pageId: resolvedId, versions }, null, 2)); + if (wantsJson(options)) { + emitJson({ pageId: resolvedId, versions }); } else { const max = versions.length ? Math.max(...versions.map(v => v.number)) : 0; console.log(chalk.blue(`Versions for page ${resolvedId} (${versions.length} total):`)); diff --git a/bin/confluence.js b/bin/confluence.js index 17f376e..fa83da5 100755 --- a/bin/confluence.js +++ b/bin/confluence.js @@ -17,6 +17,7 @@ const registerCommentCommands = require('./commands/comment'); const registerExportCommand = require('./commands/export'); const registerApiCommand = require('./commands/api'); const { readStdin } = require('../lib/stdin-utils'); +const { emitJson, jsonRequested } = require('../lib/output'); function assertWritable(config) { if (config.readOnly) { @@ -85,7 +86,7 @@ function withClient(commandName, handler, { writable = false, onError = null } = const config = getConfig(getProfileName()); if (writable) assertWritable(config); const client = new ConfluenceClient(config); - await handler({ client, config, analytics }, ...actionArgs); + await handler({ client, config, analytics, emitJson, wantsJson }, ...actionArgs); } catch (error) { const extra = onError ? (err) => onError(err, ...actionArgs) : null; handleCommandError(analytics, commandName, error, extra); @@ -111,13 +112,39 @@ program .name('confluence') .description('CLI tool for Atlassian Confluence') .version(pkg.version) - .option('--profile <name>', 'Use a specific configuration profile'); + .option('--profile <name>', 'Use a specific configuration profile') + .option('--json', 'Output raw JSON to stdout (for scripting / piping to jq)'); // Helper: resolve profile name from global --profile flag function getProfileName() { return program.opts().profile || undefined; } +// Helper: resolve JSON output mode from the global --json flag, falling back to +// the deprecated per-command "--format json" (which warns once on stderr). +function wantsJson(options) { + return jsonRequested(program.opts().json, options); +} + +// Commands that honor the global --json flag. Passing --json to anything else is +// a usage error — fail loud instead of silently emitting human-readable output. +const JSON_COMMANDS = new Set([ + 'info', 'search', 'spaces', 'find', 'children', + 'versions', 'comments', 'attachments', + 'property-list', 'property-get', 'property-set', + 'api', // already emits raw JSON +]); + +program.hook('preAction', (thisCommand, actionCommand) => { + if (program.opts().json && !JSON_COMMANDS.has(actionCommand.name())) { + console.error(chalk.red( + `Error: --json is not supported by "${actionCommand.name()}". ` + + `Supported commands: ${[...JSON_COMMANDS].join(', ')}.` + )); + process.exit(1); + } +}); + // Init command program .command('init') @@ -153,12 +180,12 @@ program program .command('info <pageId>') .description('Get information about a Confluence page') - .option('-f, --format <format>', 'Output format (text, json)', 'text') - .action(withClient('info', async ({ client, analytics }, pageId, options) => { + .option('-f, --format <format>', 'Output format (text). "json" is deprecated — use --json', 'text') + .action(withClient('info', async ({ client, analytics, wantsJson, emitJson }, pageId, options) => { const info = await client.getPageInfo(pageId); - if ((options.format || 'text').toLowerCase() === 'json') { - console.log(JSON.stringify(info, null, 2)); + if (wantsJson(options)) { + emitJson(info); } else { console.log(chalk.blue('Page Information:')); console.log(`Title: ${chalk.green(info.title)}`); @@ -179,7 +206,7 @@ program .option('-l, --limit <limit>', 'Limit number of results', '10') .option('--start <start>', 'Start index for results', '0') .option('--cql', 'Pass query as raw CQL instead of text search') - .action(withClient('search', async ({ client, analytics }, query, options) => { + .action(withClient('search', async ({ client, analytics, wantsJson, emitJson }, query, options) => { const start = parseInt(options.start, 10); if (Number.isNaN(start) || start < 0) { throw new Error('Start must be a non-negative number.'); @@ -187,6 +214,12 @@ program const results = await client.search(query, parseInt(options.limit), options.cql, start); + if (wantsJson(options)) { + emitJson({ query, start, resultCount: results.length, results }); + analytics.track('search', true); + return; + } + if (results.length === 0) { console.log(chalk.yellow('No results found.')); analytics.track('search', true); @@ -209,10 +242,16 @@ program .description('List Confluence spaces') .option('-l, --limit <limit>', 'Maximum total spaces to return across paginated requests', '500') .option('--all', 'Fetch every space, paginating through all results (overrides --limit)') - .action(withClient('spaces', async ({ client, analytics }, options) => { + .action(withClient('spaces', async ({ client, analytics, wantsJson, emitJson }, options) => { const maxResults = options.all ? null : parseInt(options.limit); const spaces = await client.getSpaces(maxResults); + if (wantsJson(options)) { + emitJson({ spaceCount: spaces.length, spaces }); + analytics.track('spaces', true); + return; + } + console.log(chalk.blue(`Available spaces (${spaces.length}):`)); spaces.forEach(space => { console.log(`${chalk.green(space.key)} - ${space.name}`); @@ -479,9 +518,15 @@ program .command('find <title>') .description('Find a page by title') .option('-s, --space <spaceKey>', 'Limit search to specific space') - .action(withClient('find', async ({ client, analytics }, title, options) => { + .action(withClient('find', async ({ client, analytics, wantsJson, emitJson }, title, options) => { const pageInfo = await client.findPageByTitle(title, options.space); + if (wantsJson(options)) { + emitJson(pageInfo); + analytics.track('find', true); + return; + } + console.log(chalk.blue('Page found:')); console.log(`Title: ${chalk.green(pageInfo.title)}`); console.log(`ID: ${chalk.green(pageInfo.id)}`); @@ -619,11 +664,12 @@ program .description('List child pages of a Confluence page') .option('-r, --recursive', 'List all descendants recursively', false) .option('--max-depth <number>', 'Maximum depth for recursive listing', '10') - .option('--format <format>', 'Output format (list, tree, json)', 'list') + .option('--format <format>', 'Output format (list, tree). "json" is deprecated — use --json', 'list') .option('--show-url', 'Show page URLs', false) .option('--show-id', 'Show page IDs', false) - .action(withClient('children', async ({ client, config, analytics }, pageId, options) => { + .action(withClient('children', async ({ client, config, analytics, wantsJson, emitJson }, pageId, options) => { const format = (options.format || 'list').toLowerCase(); + const jsonMode = wantsJson(options); // Extract page ID from URL if needed const resolvedPageId = await client.extractPageId(pageId); @@ -635,19 +681,19 @@ program children = await client.getAllDescendantPages( resolvedPageId, maxDepth, - { includeAncestors: format === 'json' } + { includeAncestors: jsonMode } ); } else { children = await client.getChildPages(resolvedPageId); } if (children.length === 0) { - if (format === 'json') { - console.log(JSON.stringify({ + if (jsonMode) { + emitJson({ pageId: String(resolvedPageId), childCount: 0, children: [] - }, null, 2)); + }); } else { console.log(chalk.yellow('No child pages found.')); } @@ -655,7 +701,7 @@ program return; } - if (format === 'json') { + if (jsonMode) { // JSON output const output = { pageId: String(resolvedPageId), @@ -683,7 +729,7 @@ program return record; }) }; - console.log(JSON.stringify(output, null, 2)); + emitJson(output); } else if (format === 'tree' && options.recursive) { // Tree format (only for recursive mode) const pageInfo = await client.getPageInfo(resolvedPageId); diff --git a/lib/output.js b/lib/output.js new file mode 100644 index 0000000..dc20e7e --- /dev/null +++ b/lib/output.js @@ -0,0 +1,36 @@ +'use strict'; + +const chalk = require('chalk'); + +// Single place JSON output is produced, so piping to tools like jq stays clean: +// pretty-printed, no colors, no human-readable preamble. Uses console.log (not +// process.stdout.write) so it lands on stdout and stays test-spy friendly. +function emitJson(data) { + console.log(JSON.stringify(data, null, 2)); +} + +let deprecationWarned = false; + +// Resolve whether a command should emit JSON. +// - `globalJson` is the value of the global `--json` flag (the canonical form). +// - The per-command `--format json` form still works but is deprecated: it +// prints a one-time warning to stderr (never stdout, so piped JSON stays +// parseable) and is honored for backward compatibility. +function jsonRequested(globalJson, options = {}) { + if (globalJson) { + return true; + } + const format = typeof options.format === 'string' ? options.format.toLowerCase() : ''; + if (format === 'json') { + if (!deprecationWarned) { + deprecationWarned = true; + console.error(chalk.yellow( + 'Warning: "--format json" is deprecated and will be removed in a future major version. Use the global "--json" flag instead.' + )); + } + return true; + } + return false; +} + +module.exports = { emitJson, jsonRequested }; diff --git a/tests/json-output.test.js b/tests/json-output.test.js new file mode 100644 index 0000000..25f4bed --- /dev/null +++ b/tests/json-output.test.js @@ -0,0 +1,160 @@ +describe('Global --json output flag', () => { + async function loadCli(clientOverrides = {}) { + jest.resetModules(); + + const client = { + readPage: jest.fn(), + getPageInfo: jest.fn(), + extractPageId: jest.fn(async (pageId) => String(pageId)), + search: jest.fn(), + getSpaces: jest.fn(), + findPageByTitle: jest.fn(), + buildUrl: jest.fn((value) => value), + webUrlPrefix: '/wiki', + ...clientOverrides + }; + + const ConfluenceClient = jest.fn(() => client); + const getConfig = jest.fn(() => ({ + domain: 'test.atlassian.net', + token: 'test-token' + })); + const track = jest.fn(); + + jest.doMock('../lib/confluence-client', () => ConfluenceClient); + jest.doMock('../lib/config', () => ({ + getConfig, + initConfig: jest.fn(), + listProfiles: jest.fn(), + setActiveProfile: jest.fn(), + deleteProfile: jest.fn(), + isValidProfileName: jest.fn(() => true) + })); + jest.doMock('../lib/analytics', () => { + return class Analytics { + track(...args) { + track(...args); + } + }; + }); + + let cli; + jest.isolateModules(() => { + cli = require('../bin/confluence.js'); + }); + + return { program: cli.program, client, getConfig, track }; + } + + async function runCli(program, args) { + return program.parseAsync(args, { from: 'user' }); + } + + afterEach(() => { + jest.restoreAllMocks(); + jest.resetModules(); + }); + + test('info --json prints structured metadata with no deprecation warning', async () => { + const { program } = await loadCli({ + getPageInfo: jest.fn(async () => ({ + id: '123', title: 'Page', type: 'page', status: 'current', + space: { key: 'ENG', name: 'Engineering' } + })) + }); + const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + await runCli(program, ['info', '123', '--json']); + + const output = JSON.parse(logSpy.mock.calls[0][0]); + expect(output).toMatchObject({ id: '123', title: 'Page' }); + const warned = errSpy.mock.calls.some(call => String(call[0]).includes('deprecated')); + expect(warned).toBe(false); + }); + + test('info --format json still works but warns on stderr (deprecated), stdout stays clean JSON', async () => { + const { program } = await loadCli({ + getPageInfo: jest.fn(async () => ({ id: '123', title: 'Page' })) + }); + const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + await runCli(program, ['info', '123', '--format', 'json']); + + // stdout: the very first console.log is valid, parseable JSON (no preamble) + const output = JSON.parse(logSpy.mock.calls[0][0]); + expect(output).toMatchObject({ id: '123', title: 'Page' }); + // stderr: a deprecation warning pointing at --json + const warning = errSpy.mock.calls.map(c => String(c[0])).find(m => m.includes('deprecated')); + expect(warning).toBeDefined(); + expect(warning).toContain('--json'); + }); + + test('search --json wraps results with a count', async () => { + const { program } = await loadCli({ + search: jest.fn(async () => [ + { id: '1', title: 'Alpha' }, + { id: '2', title: 'Beta' } + ]) + }); + const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + + await runCli(program, ['search', 'hello', '--json']); + + const output = JSON.parse(logSpy.mock.calls[0][0]); + expect(output).toMatchObject({ query: 'hello', resultCount: 2 }); + expect(output.results).toHaveLength(2); + }); + + test('search --json emits an empty result set cleanly (no "No results" preamble)', async () => { + const { program } = await loadCli({ search: jest.fn(async () => []) }); + const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + + await runCli(program, ['search', 'nothing', '--json']); + + expect(logSpy.mock.calls).toHaveLength(1); + const output = JSON.parse(logSpy.mock.calls[0][0]); + expect(output).toMatchObject({ resultCount: 0, results: [] }); + }); + + test('spaces --json wraps spaces with a count', async () => { + const { program } = await loadCli({ + getSpaces: jest.fn(async () => [{ key: 'ENG', name: 'Engineering' }]) + }); + const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + + await runCli(program, ['spaces', '--json']); + + const output = JSON.parse(logSpy.mock.calls[0][0]); + expect(output).toMatchObject({ spaceCount: 1 }); + expect(output.spaces[0]).toMatchObject({ key: 'ENG' }); + }); + + test('--json on an unsupported command fails loudly instead of being silently ignored', async () => { + const { program } = await loadCli(); + const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => { + throw new Error('process.exit called'); + }); + + await expect(runCli(program, ['read', '123', '--json'])).rejects.toThrow('process.exit called'); + expect(exitSpy).toHaveBeenCalledWith(1); + const msg = errSpy.mock.calls.map(c => String(c[0])).find(m => m.includes('--json')); + expect(msg).toContain('not supported'); + }); + + test('find --json prints the page object directly', async () => { + const { program } = await loadCli({ + findPageByTitle: jest.fn(async () => ({ + id: '999', title: 'Found', space: { key: 'ENG', name: 'Engineering' }, url: '/x' + })) + }); + const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + + await runCli(program, ['find', 'Found', '--json']); + + const output = JSON.parse(logSpy.mock.calls[0][0]); + expect(output).toMatchObject({ id: '999', title: 'Found' }); + }); +}); From 8a7e00af03799d34e88eea704887431cb756dc0f Mon Sep 17 00:00:00 2001 From: "heecheol.park" <heecheol.park@linecorp.com> Date: Sat, 20 Jun 2026 22:09:17 +0900 Subject: [PATCH 2/2] fix(deps): resolve form-data and markdown-it advisories via npm audit fix Bumps transitive form-data to 4.0.6 (CRLF injection, GHSA-hmw2-7cc7-3qxx) and markdown-it to 14.2.0 (quadratic-complexity DoS, GHSA-6v5v-wf23-fmfq). Lockfile-only and non-breaking; clears the production npm audit enforced by the CI security job. --- package-lock.json | 366 +++++++++++++++++++++++++--------------------- 1 file changed, 198 insertions(+), 168 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6341225..3e3c51f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,28 +33,14 @@ "node": ">=18.0.0" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -63,9 +49,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz", - "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -73,22 +59,22 @@ } }, "node_modules/@babel/core": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", - "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.4", - "@babel/parser": "^7.27.4", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.27.4", - "@babel/types": "^7.27.3", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "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", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -104,16 +90,16 @@ } }, "node_modules/@babel/generator": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", - "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.5", - "@babel/types": "^7.27.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@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" }, "engines": { @@ -121,14 +107,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -137,30 +123,40 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -180,9 +176,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -190,9 +186,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -200,9 +196,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -210,27 +206,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", - "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.27.6" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", - "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -479,58 +475,48 @@ } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", - "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", - "@babel/parser": "^7.27.4", - "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@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", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/types": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", - "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1165,34 +1151,31 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6.0.0" + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", "engines": { @@ -1207,9 +1190,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -1684,6 +1667,19 @@ ], "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.38", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", + "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -1720,9 +1716,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "dev": true, "funding": [ { @@ -1740,10 +1736,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -1827,9 +1824,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001726", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz", - "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==", + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", "dev": true, "funding": [ { @@ -2244,9 +2241,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.176", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.176.tgz", - "integrity": "sha512-2nDK9orkm7M9ZZkjO3PjbEd3VUulQLyg5T9O3enJdFvUg46Hzd4DUvTvAuEgbdHYXyFsiG4A5sO9IzToMH1cDg==", + "version": "1.5.376", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.376.tgz", + "integrity": "sha512-cUVA7/RvbFTEuw/i3obUwDTRIXojaxkResf+ibByPFxjc6XK3VNtcQXV0NSbAlJ0FMjcJGgftVVB4Qo184EXvA==", "dev": true, "license": "ISC" }, @@ -2717,16 +2714,16 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -2947,9 +2944,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4003,10 +4000,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -4130,9 +4137,19 @@ "license": "MIT" }, "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz", + "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "uc.micro": "^2.0.0" @@ -4233,14 +4250,24 @@ } }, "node_modules/markdown-it": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", - "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz", + "integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", - "linkify-it": "^5.0.0", + "linkify-it": "^5.0.1", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" @@ -4355,11 +4382,14 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.48", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz", + "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/normalize-path": { "version": "3.0.0", @@ -5240,9 +5270,9 @@ "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ {