@testomatio/reporter 2.8.2 → 2.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/bin/cli.js CHANGED
@@ -19,14 +19,14 @@ const filesize_1 = require("filesize");
19
19
  const dotenv_1 = __importDefault(require("dotenv"));
20
20
  const replay_js_1 = __importDefault(require("../replay.js"));
21
21
  const log_js_1 = require("../utils/log.js");
22
+ const pipe_utils_js_1 = require("../utils/pipe_utils.js");
22
23
  const debug = (0, debug_1.default)('@testomatio/reporter:cli');
23
24
  const version = (0, utils_js_1.getPackageVersion)();
24
- console.log(picocolors_1.default.cyan(picocolors_1.default.bold(` 🤩 Testomat.io Reporter v${version}`)));
25
25
  const program = new commander_1.Command();
26
26
  program
27
27
  .version(version)
28
28
  .option('--env-file <envfile>', 'Load environment variables from env file')
29
- .hook('preAction', thisCommand => {
29
+ .hook('preAction', (thisCommand, actionCommand) => {
30
30
  const opts = thisCommand.opts();
31
31
  if (opts.envFile) {
32
32
  dotenv_1.default.config({ path: opts.envFile });
@@ -34,6 +34,18 @@ program
34
34
  else {
35
35
  dotenv_1.default.config();
36
36
  }
37
+ // --filter-list produces a machine-readable test list on stdout, so route
38
+ // remaining output to stderr, skip the banner, and silence info-level
39
+ // logs so the terminal isn't flooded with progress noise.
40
+ // Set TESTOMATIO_LOG_LEVEL=INFO to re-enable progress logs for debugging.
41
+ const subOpts = actionCommand.opts();
42
+ if (subOpts.filterList || subOpts.format) {
43
+ process.env.TESTOMATIO_LOG_STDERR = '1';
44
+ process.env.TESTOMATIO_LOG_LEVEL ||= 'WARN';
45
+ }
46
+ else {
47
+ console.log(picocolors_1.default.cyan(picocolors_1.default.bold(` 🤩 Testomat.io Reporter v${version}`)));
48
+ }
37
49
  });
38
50
  program
39
51
  .command('start')
@@ -77,6 +89,7 @@ program
77
89
  .argument('[command]', 'Test runner command')
78
90
  .option('--filter <filter>', 'Additional execution filter')
79
91
  .option('--filter-list <filter>', 'Get a list of all tests by filter before running')
92
+ .option('--format <format>', 'Machine-readable output format for --filter-list (grep, json, newline, ids)')
80
93
  .option('--kind <type>', 'Specify run type: automated, manual, or mixed')
81
94
  .action(async (command, opts) => {
82
95
  const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config_js_1.config.TESTOMATIO;
@@ -86,7 +99,7 @@ program
86
99
  log_js_1.log.info('Filtering tests...');
87
100
  // Example of use: npx @testomatio/reporter run "npx jest" --filter "testomatio:tag-name=frontend"
88
101
  // Example of use: npx @testomatio/reporter run "npx jest" --filter "coverage:file=coverage.yml"
89
- // Example of use: npx @testomatio/reporter run "npx jest" --filter-list "coverage:file=coverage.yml"
102
+ // Example of use: npx @testomatio/reporter run --filter-list "coverage:file=coverage.yml" --format grep
90
103
  const [pipe, ...optsArray] = opts?.filter ? opts?.filter.split(':') : opts?.filterList.split(':');
91
104
  const pipeOptions = optsArray.join(':');
92
105
  const prepareRunParams = { pipe, pipeOptions };
@@ -96,26 +109,32 @@ program
96
109
  try {
97
110
  const tests = await client.prepareRun(prepareRunParams);
98
111
  if (!tests || tests.length === 0) {
99
- log_js_1.log.info(picocolors_1.default.yellow('No tests found.'));
112
+ log_js_1.log.warn(picocolors_1.default.yellow('No tests found.'));
113
+ // Exit non-zero on --filter-list so scripts can detect "nothing to run"
114
+ // via $? and skip launching the runner.
115
+ if (opts.filterList)
116
+ process.exit(1);
100
117
  return;
101
118
  }
102
- const pattern = `(${tests.join('|')})`;
103
- const filteredCommand = (0, utils_js_1.applyFilter)(command, tests);
104
- debug(`Execution pattern: "${pattern}"`);
105
119
  if (opts.filterList) {
106
- if (command)
107
- log_js_1.log.info(picocolors_1.default.green(`Full Running Command: ${filteredCommand}`));
108
- console.log();
109
- console.log(`Grep string:`);
110
- console.log(`${tests.join(', ')}`);
120
+ const out = (0, pipe_utils_js_1.formatFilterListIds)(tests, opts.format || 'ids');
121
+ if (out)
122
+ console.log(out);
123
+ // Show the runnable-command hint only in interactive mode (no explicit --format).
124
+ // When --format is set the user is scripting and doesn't need stderr noise.
125
+ if (command && !opts.format) {
126
+ log_js_1.log.info(picocolors_1.default.green(`Full Running Command: ${(0, utils_js_1.applyFilter)(command, tests)}`));
127
+ }
111
128
  return;
112
129
  }
113
130
  if (command && command.split) {
114
- command = filteredCommand;
131
+ command = (0, utils_js_1.applyFilter)(command, tests);
115
132
  }
116
133
  }
117
134
  catch (err) {
118
- log_js_1.log.info(err.message || err);
135
+ log_js_1.log.error(err.message || err);
136
+ if (opts.filterList)
137
+ process.exit(1);
119
138
  return;
120
139
  }
121
140
  }
@@ -54,7 +54,7 @@ class CoveragePipe {
54
54
  this.branch = options?.diff || process.env.COVERAGE_BRANCH || this.#GIT.default_branch;
55
55
  this.isBranchDefault = !options.diff && !process.env.COVERAGE_BRANCH;
56
56
  if (this.isBranchDefault) {
57
- console.log(constants_js_1.APP_PREFIX, `🟡 No "diff" branch provided. That's why we use default one = "${this.branch}".\n` +
57
+ log_js_1.log.info(`🟡 No "diff" branch provided. That's why we use default one = "${this.branch}".\n` +
58
58
  '👉 You can set it via --filter "coverage:file=coverage.yml,diff=your-branch"');
59
59
  }
60
60
  // Client config section
@@ -125,7 +125,7 @@ class CoveragePipe {
125
125
  log_js_1.log.info(`Matched files: ${[...lines].join(', ')}`);
126
126
  }
127
127
  if (lines.size === 0) {
128
- log_js_1.log.info('â„šī¸ No matching entries in coverage file for provided Git changes.');
128
+ log_js_1.log.warn('â„šī¸ No matching entries in coverage file for provided Git changes.');
129
129
  return [];
130
130
  }
131
131
  // Step 3: Handle tag labels tests from the server
@@ -136,13 +136,13 @@ class CoveragePipe {
136
136
  const tests = await this.#getTestomatioTestsByParam(tagType, tag);
137
137
  if (!tests)
138
138
  return [];
139
- console.log(constants_js_1.APP_PREFIX, `✅ We found ${tests.length === 1 ? 'one entry' : `${tests.length} (test/suite) entries`}` +
139
+ log_js_1.log.info(`✅ We found ${tests.length === 1 ? 'one entry' : `${tests.length} (test/suite) entries`}` +
140
140
  ' in Testomat.io service side.');
141
141
  tests.forEach(testId => this.tests.add(testId));
142
142
  }
143
143
  }
144
144
  if (this.tests.size === 0 && this.suiteIds.size === 0) {
145
- log_js_1.log.info('â„šī¸ No tests found for execution based on Git changes.');
145
+ log_js_1.log.warn('â„šī¸ No tests found for execution based on Git changes.');
146
146
  return [];
147
147
  }
148
148
  this.results = [...this.tests, ...this.suiteIds];
@@ -201,7 +201,7 @@ class CoveragePipe {
201
201
  ...q,
202
202
  });
203
203
  if (!Array.isArray(resp.data?.tests) && resp.data?.tests?.length === 0) {
204
- log_js_1.log.info(`🔍 No test by ${type}=${id} were found on the Testomat.io server side!`);
204
+ log_js_1.log.warn(`🔍 No test by ${type}=${id} were found on the Testomat.io server side!`);
205
205
  return undefined;
206
206
  }
207
207
  return resp.data.tests;
@@ -276,7 +276,7 @@ class CoveragePipe {
276
276
  log_js_1.log.error(err.message);
277
277
  return undefined;
278
278
  }
279
- log_js_1.log.error(`â„šī¸ We will use '${cmd}' Git command.`);
279
+ log_js_1.log.info(`We will use '${cmd}' Git command.`);
280
280
  try {
281
281
  // For clear unit testing process -> Like test_defaultGitChangedFile = todomvc-tests/edit-todos_test.js
282
282
  if (this.isDefaultGitChanges) {
@@ -285,7 +285,7 @@ class CoveragePipe {
285
285
  else {
286
286
  this.changedFiles = this.#getChangedFilesFromGit(cmd);
287
287
  if (this.changedFiles.length === 0) {
288
- console.log(constants_js_1.APP_PREFIX, 'â„šī¸ No files changed in the latest Git commit. Skipping coverage processing.');
288
+ log_js_1.log.warn('â„šī¸ No files changed in the latest Git commit. Skipping coverage processing.');
289
289
  return undefined;
290
290
  }
291
291
  }
@@ -314,18 +314,18 @@ class CoveragePipe {
314
314
  validateCoverageFile() {
315
315
  // Validate the presence of the coverage filepath
316
316
  if (!fs_1.default.existsSync(this.coverageFilePath)) {
317
- log_js_1.log.info('❌ Coverage file not found:', this.coverageFilePath);
317
+ log_js_1.log.error('❌ Coverage file not found:', this.coverageFilePath);
318
318
  return undefined;
319
319
  }
320
320
  // Ensure the given path is a file (not a directory or other type)
321
321
  const stat = fs_1.default.statSync(this.coverageFilePath);
322
322
  if (!stat.isFile()) {
323
- log_js_1.log.info('❌ Provided coverage path is not a file:', this.coverageFilePath);
323
+ log_js_1.log.error('❌ Provided coverage path is not a file:', this.coverageFilePath);
324
324
  return undefined;
325
325
  }
326
326
  // Validate the file extension to be ".yml" to ensure it's a YAML file
327
327
  if (path_1.default.extname(this.coverageFilePath) !== ".yml") {
328
- log_js_1.log.info('❌ Coverage file must have a .yml extension:', this.coverageFilePath);
328
+ log_js_1.log.error('❌ Coverage file must have a .yml extension:', this.coverageFilePath);
329
329
  return undefined;
330
330
  }
331
331
  debug('Coverage file validation is OK!');
package/lib/utils/log.js CHANGED
@@ -46,7 +46,8 @@ function shouldLog(messageLevel) {
46
46
  */
47
47
  function info(...args) {
48
48
  if (shouldLog(exports.LOG_LEVELS.INFO)) {
49
- console.log(constants_js_1.APP_PREFIX, ...args);
49
+ const fn = process.env.TESTOMATIO_LOG_STDERR === '1' ? console.error : console.log;
50
+ fn(constants_js_1.APP_PREFIX, ...args);
50
51
  }
51
52
  }
52
53
  /**
@@ -54,3 +54,13 @@ export function fullName(t: object): string;
54
54
  * => Returns: { foo: 'bar', baz: 'qux' }
55
55
  */
56
56
  export function parsePipeOptions(optionsStr?: string): any;
57
+ /**
58
+ * Format a list of test IDs for `--filter-list` machine-readable output.
59
+ * Used when the CLI `--format` option is passed,
60
+ * e.g. `--filter-list "coverage:file=..." --format grep`.
61
+ *
62
+ * @param {string[]} ids
63
+ * @param {'grep'|'json'|'newline'|'ids'} format
64
+ * @returns {string} Empty string if no ids; otherwise the formatted output.
65
+ */
66
+ export function formatFilterListIds(ids: string[], format: "grep" | "json" | "newline" | "ids"): string;
@@ -7,6 +7,7 @@ exports.setS3Credentials = setS3Credentials;
7
7
  exports.statusEmoji = statusEmoji;
8
8
  exports.fullName = fullName;
9
9
  exports.parsePipeOptions = parsePipeOptions;
10
+ exports.formatFilterListIds = formatFilterListIds;
10
11
  const log_js_1 = require("./log.js");
11
12
  /**
12
13
  * Set S3 credentials from the provided artifacts object.
@@ -161,6 +162,26 @@ function parsePipeOptions(optionsStr) {
161
162
  }
162
163
  return options;
163
164
  }
165
+ /**
166
+ * Format a list of test IDs for `--filter-list` machine-readable output.
167
+ * Used when the CLI `--format` option is passed,
168
+ * e.g. `--filter-list "coverage:file=..." --format grep`.
169
+ *
170
+ * @param {string[]} ids
171
+ * @param {'grep'|'json'|'newline'|'ids'} format
172
+ * @returns {string} Empty string if no ids; otherwise the formatted output.
173
+ */
174
+ function formatFilterListIds(ids, format) {
175
+ if (!ids || ids.length === 0)
176
+ return '';
177
+ switch (format) {
178
+ case 'grep': return `(${ids.join('|')})`;
179
+ case 'json': return JSON.stringify(ids);
180
+ case 'newline': return ids.join('\n');
181
+ case 'ids':
182
+ default: return ids.join(',');
183
+ }
184
+ }
164
185
 
165
186
  module.exports.updateFilterType = updateFilterType;
166
187
 
@@ -175,3 +196,5 @@ module.exports.statusEmoji = statusEmoji;
175
196
  module.exports.fullName = fullName;
176
197
 
177
198
  module.exports.parsePipeOptions = parsePipeOptions;
199
+
200
+ module.exports.formatFilterListIds = formatFilterListIds;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.8.2",
3
+ "version": "2.8.4",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"
package/src/bin/cli.js CHANGED
@@ -15,22 +15,34 @@ import { filesize as prettyBytes } from 'filesize';
15
15
  import dotenv from 'dotenv';
16
16
  import Replay from '../replay.js';
17
17
  import { log } from '../utils/log.js';
18
+ import { formatFilterListIds } from '../utils/pipe_utils.js';
18
19
 
19
20
  const debug = createDebugMessages('@testomatio/reporter:cli');
20
21
  const version = getPackageVersion();
21
- console.log(pc.cyan(pc.bold(` 🤩 Testomat.io Reporter v${version}`)));
22
22
  const program = new Command();
23
23
 
24
24
  program
25
25
  .version(version)
26
26
  .option('--env-file <envfile>', 'Load environment variables from env file')
27
- .hook('preAction', thisCommand => {
27
+ .hook('preAction', (thisCommand, actionCommand) => {
28
28
  const opts = thisCommand.opts();
29
29
  if (opts.envFile) {
30
30
  dotenv.config({ path: opts.envFile });
31
31
  } else {
32
32
  dotenv.config();
33
33
  }
34
+
35
+ // --filter-list produces a machine-readable test list on stdout, so route
36
+ // remaining output to stderr, skip the banner, and silence info-level
37
+ // logs so the terminal isn't flooded with progress noise.
38
+ // Set TESTOMATIO_LOG_LEVEL=INFO to re-enable progress logs for debugging.
39
+ const subOpts = actionCommand.opts();
40
+ if (subOpts.filterList || subOpts.format) {
41
+ process.env.TESTOMATIO_LOG_STDERR = '1';
42
+ process.env.TESTOMATIO_LOG_LEVEL ||= 'WARN';
43
+ } else {
44
+ console.log(pc.cyan(pc.bold(` 🤩 Testomat.io Reporter v${version}`)));
45
+ }
34
46
  });
35
47
 
36
48
  program
@@ -83,6 +95,7 @@ program
83
95
  .argument('[command]', 'Test runner command')
84
96
  .option('--filter <filter>', 'Additional execution filter')
85
97
  .option('--filter-list <filter>', 'Get a list of all tests by filter before running')
98
+ .option('--format <format>', 'Machine-readable output format for --filter-list (grep, json, newline, ids)')
86
99
  .option('--kind <type>', 'Specify run type: automated, manual, or mixed')
87
100
  .action(async (command, opts) => {
88
101
  const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
@@ -93,7 +106,7 @@ program
93
106
  log.info('Filtering tests...');
94
107
  // Example of use: npx @testomatio/reporter run "npx jest" --filter "testomatio:tag-name=frontend"
95
108
  // Example of use: npx @testomatio/reporter run "npx jest" --filter "coverage:file=coverage.yml"
96
- // Example of use: npx @testomatio/reporter run "npx jest" --filter-list "coverage:file=coverage.yml"
109
+ // Example of use: npx @testomatio/reporter run --filter-list "coverage:file=coverage.yml" --format grep
97
110
  const [pipe, ...optsArray] = opts?.filter ? opts?.filter.split(':') : opts?.filterList.split(':');
98
111
  const pipeOptions = optsArray.join(':');
99
112
 
@@ -106,29 +119,31 @@ program
106
119
  const tests = await client.prepareRun(prepareRunParams);
107
120
 
108
121
  if (!tests || tests.length === 0) {
109
- log.info( pc.yellow('No tests found.'));
122
+ log.warn( pc.yellow('No tests found.'));
123
+ // Exit non-zero on --filter-list so scripts can detect "nothing to run"
124
+ // via $? and skip launching the runner.
125
+ if (opts.filterList) process.exit(1);
110
126
  return;
111
127
  }
112
128
 
113
- const pattern = `(${tests.join('|')})`;
114
- const filteredCommand = applyFilter(command, tests);
115
-
116
- debug(`Execution pattern: "${pattern}"`);
117
-
118
- if(opts.filterList) {
119
- if (command) log.info(pc.green(`Full Running Command: ${filteredCommand}`));
120
- console.log();
121
- console.log(`Grep string:`);
122
- console.log(`${tests.join(', ')}`);
129
+ if (opts.filterList) {
130
+ const out = formatFilterListIds(tests, opts.format || 'ids');
131
+ if (out) console.log(out);
132
+ // Show the runnable-command hint only in interactive mode (no explicit --format).
133
+ // When --format is set the user is scripting and doesn't need stderr noise.
134
+ if (command && !opts.format) {
135
+ log.info(pc.green(`Full Running Command: ${applyFilter(command, tests)}`));
136
+ }
123
137
  return;
124
138
  }
125
139
 
126
140
  if (command && command.split) {
127
- command = filteredCommand;
141
+ command = applyFilter(command, tests);
128
142
  }
129
143
  }
130
144
  catch (err) {
131
- log.info( err.message || err);
145
+ log.error( err.message || err);
146
+ if (opts.filterList) process.exit(1);
132
147
  return;
133
148
  }
134
149
  }
@@ -57,8 +57,7 @@ class CoveragePipe { // or Changes for the future???
57
57
  this.isBranchDefault = !options.diff && !process.env.COVERAGE_BRANCH;
58
58
 
59
59
  if (this.isBranchDefault) {
60
- console.log(
61
- APP_PREFIX,
60
+ log.info(
62
61
  `🟡 No "diff" branch provided. That's why we use default one = "${this.branch}".\n` +
63
62
  '👉 You can set it via --filter "coverage:file=coverage.yml,diff=your-branch"'
64
63
  );
@@ -141,7 +140,7 @@ class CoveragePipe { // or Changes for the future???
141
140
  }
142
141
 
143
142
  if (lines.size === 0) {
144
- log.info( 'â„šī¸ No matching entries in coverage file for provided Git changes.');
143
+ log.warn( 'â„šī¸ No matching entries in coverage file for provided Git changes.');
145
144
  return [];
146
145
  }
147
146
 
@@ -154,8 +153,7 @@ class CoveragePipe { // or Changes for the future???
154
153
 
155
154
  if (!tests) return [];
156
155
 
157
- console.log(
158
- APP_PREFIX,
156
+ log.info(
159
157
  `✅ We found ${tests.length === 1 ? 'one entry' : `${tests.length} (test/suite) entries`}` +
160
158
  ' in Testomat.io service side.'
161
159
  );
@@ -165,7 +163,7 @@ class CoveragePipe { // or Changes for the future???
165
163
  }
166
164
 
167
165
  if (this.tests.size === 0 && this.suiteIds.size === 0) {
168
- log.info( 'â„šī¸ No tests found for execution based on Git changes.');
166
+ log.warn( 'â„šī¸ No tests found for execution based on Git changes.');
169
167
  return [];
170
168
  }
171
169
 
@@ -236,7 +234,7 @@ class CoveragePipe { // or Changes for the future???
236
234
  });
237
235
 
238
236
  if (!Array.isArray(resp.data?.tests) && resp.data?.tests?.length === 0) {
239
- log.info( `🔍 No test by ${type}=${id} were found on the Testomat.io server side!`);
237
+ log.warn( `🔍 No test by ${type}=${id} were found on the Testomat.io server side!`);
240
238
 
241
239
  return undefined;
242
240
  }
@@ -324,7 +322,7 @@ class CoveragePipe { // or Changes for the future???
324
322
  return undefined;
325
323
  }
326
324
 
327
- log.error( `â„šī¸ We will use '${cmd}' Git command.`);
325
+ log.info( `We will use '${cmd}' Git command.`);
328
326
 
329
327
  try {
330
328
  // For clear unit testing process -> Like test_defaultGitChangedFile = todomvc-tests/edit-todos_test.js
@@ -335,10 +333,7 @@ class CoveragePipe { // or Changes for the future???
335
333
  this.changedFiles = this.#getChangedFilesFromGit(cmd);
336
334
 
337
335
  if (this.changedFiles.length === 0) {
338
- console.log(
339
- APP_PREFIX,
340
- 'â„šī¸ No files changed in the latest Git commit. Skipping coverage processing.'
341
- );
336
+ log.warn('â„šī¸ No files changed in the latest Git commit. Skipping coverage processing.');
342
337
 
343
338
  return undefined;
344
339
  }
@@ -370,20 +365,20 @@ class CoveragePipe { // or Changes for the future???
370
365
  validateCoverageFile() {
371
366
  // Validate the presence of the coverage filepath
372
367
  if (!fs.existsSync(this.coverageFilePath)) {
373
- log.info( '❌ Coverage file not found:', this.coverageFilePath);
368
+ log.error( '❌ Coverage file not found:', this.coverageFilePath);
374
369
  return undefined;
375
370
  }
376
371
 
377
372
  // Ensure the given path is a file (not a directory or other type)
378
373
  const stat = fs.statSync(this.coverageFilePath);
379
374
  if (!stat.isFile()) {
380
- log.info( '❌ Provided coverage path is not a file:', this.coverageFilePath);
375
+ log.error( '❌ Provided coverage path is not a file:', this.coverageFilePath);
381
376
  return undefined;
382
377
  }
383
378
 
384
379
  // Validate the file extension to be ".yml" to ensure it's a YAML file
385
380
  if (path.extname(this.coverageFilePath) !== ".yml") {
386
- log.info( '❌ Coverage file must have a .yml extension:', this.coverageFilePath);
381
+ log.error( '❌ Coverage file must have a .yml extension:', this.coverageFilePath);
387
382
  return undefined;
388
383
  }
389
384
 
package/src/utils/log.js CHANGED
@@ -42,7 +42,8 @@ export function shouldLog(messageLevel) {
42
42
  */
43
43
  export function info(...args) {
44
44
  if (shouldLog(LOG_LEVELS.INFO)) {
45
- console.log(APP_PREFIX, ...args);
45
+ const fn = process.env.TESTOMATIO_LOG_STDERR === '1' ? console.error : console.log;
46
+ fn(APP_PREFIX, ...args);
46
47
  }
47
48
  }
48
49
 
@@ -161,12 +161,33 @@ function parsePipeOptions(optionsStr) {
161
161
  return options;
162
162
  }
163
163
 
164
- export {
165
- updateFilterType,
166
- parseFilterParams,
167
- generateFilterRequestParams,
168
- setS3Credentials,
169
- statusEmoji,
164
+ /**
165
+ * Format a list of test IDs for `--filter-list` machine-readable output.
166
+ * Used when the CLI `--format` option is passed,
167
+ * e.g. `--filter-list "coverage:file=..." --format grep`.
168
+ *
169
+ * @param {string[]} ids
170
+ * @param {'grep'|'json'|'newline'|'ids'} format
171
+ * @returns {string} Empty string if no ids; otherwise the formatted output.
172
+ */
173
+ function formatFilterListIds(ids, format) {
174
+ if (!ids || ids.length === 0) return '';
175
+ switch (format) {
176
+ case 'grep': return `(${ids.join('|')})`;
177
+ case 'json': return JSON.stringify(ids);
178
+ case 'newline': return ids.join('\n');
179
+ case 'ids':
180
+ default: return ids.join(',');
181
+ }
182
+ }
183
+
184
+ export {
185
+ updateFilterType,
186
+ parseFilterParams,
187
+ generateFilterRequestParams,
188
+ setS3Credentials,
189
+ statusEmoji,
170
190
  fullName,
171
- parsePipeOptions
191
+ parsePipeOptions,
192
+ formatFilterListIds,
172
193
  };