anz-legislation 1.2.1

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.
Files changed (95) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +23 -0
  3. package/dist/cli.d.ts +6 -0
  4. package/dist/cli.js +198 -0
  5. package/dist/client.d.ts +84 -0
  6. package/dist/client.js +492 -0
  7. package/dist/commands/batch.d.ts +5 -0
  8. package/dist/commands/batch.js +121 -0
  9. package/dist/commands/cache.d.ts +5 -0
  10. package/dist/commands/cache.js +43 -0
  11. package/dist/commands/cite.d.ts +5 -0
  12. package/dist/commands/cite.js +68 -0
  13. package/dist/commands/config.d.ts +5 -0
  14. package/dist/commands/config.js +56 -0
  15. package/dist/commands/export.d.ts +8 -0
  16. package/dist/commands/export.js +169 -0
  17. package/dist/commands/generate.d.ts +10 -0
  18. package/dist/commands/generate.js +320 -0
  19. package/dist/commands/get.d.ts +5 -0
  20. package/dist/commands/get.js +99 -0
  21. package/dist/commands/help.d.ts +13 -0
  22. package/dist/commands/help.js +298 -0
  23. package/dist/commands/search.d.ts +5 -0
  24. package/dist/commands/search.js +96 -0
  25. package/dist/commands/stream.d.ts +5 -0
  26. package/dist/commands/stream.js +100 -0
  27. package/dist/config.d.ts +81 -0
  28. package/dist/config.js +209 -0
  29. package/dist/errors.d.ts +108 -0
  30. package/dist/errors.js +173 -0
  31. package/dist/mcp/server.d.ts +13 -0
  32. package/dist/mcp/server.js +428 -0
  33. package/dist/mcp-cli.d.ts +6 -0
  34. package/dist/mcp-cli.js +37 -0
  35. package/dist/models/canonical.d.ts +423 -0
  36. package/dist/models/canonical.js +92 -0
  37. package/dist/models/index.d.ts +892 -0
  38. package/dist/models/index.js +223 -0
  39. package/dist/output/index.d.ts +34 -0
  40. package/dist/output/index.js +195 -0
  41. package/dist/output/legal-metadata-publication.d.ts +18 -0
  42. package/dist/output/legal-metadata-publication.js +23 -0
  43. package/dist/providers/canonical-metadata.d.ts +3 -0
  44. package/dist/providers/canonical-metadata.js +202 -0
  45. package/dist/providers/commonwealth-provider.d.ts +27 -0
  46. package/dist/providers/commonwealth-provider.js +81 -0
  47. package/dist/providers/index.d.ts +20 -0
  48. package/dist/providers/index.js +27 -0
  49. package/dist/providers/legislation-provider.d.ts +227 -0
  50. package/dist/providers/legislation-provider.js +308 -0
  51. package/dist/providers/nz-provider.d.ts +36 -0
  52. package/dist/providers/nz-provider.js +130 -0
  53. package/dist/providers/output-adapters.d.ts +14 -0
  54. package/dist/providers/output-adapters.js +116 -0
  55. package/dist/providers/plugin-discovery.d.ts +39 -0
  56. package/dist/providers/plugin-discovery.js +91 -0
  57. package/dist/providers/plugin-loader.d.ts +86 -0
  58. package/dist/providers/plugin-loader.js +219 -0
  59. package/dist/providers/queensland-provider.d.ts +42 -0
  60. package/dist/providers/queensland-provider.js +105 -0
  61. package/dist/utils/api-optimization.d.ts +92 -0
  62. package/dist/utils/api-optimization.js +276 -0
  63. package/dist/utils/batch.d.ts +110 -0
  64. package/dist/utils/batch.js +269 -0
  65. package/dist/utils/branded-types.d.ts +0 -0
  66. package/dist/utils/branded-types.js +1 -0
  67. package/dist/utils/compatibility-matrix.d.ts +89 -0
  68. package/dist/utils/compatibility-matrix.js +214 -0
  69. package/dist/utils/config-validator.d.ts +39 -0
  70. package/dist/utils/config-validator.js +197 -0
  71. package/dist/utils/env-loader.d.ts +55 -0
  72. package/dist/utils/env-loader.js +77 -0
  73. package/dist/utils/health-monitor.d.ts +93 -0
  74. package/dist/utils/health-monitor.js +209 -0
  75. package/dist/utils/invocation.d.ts +4 -0
  76. package/dist/utils/invocation.js +33 -0
  77. package/dist/utils/logger.d.ts +94 -0
  78. package/dist/utils/logger.js +220 -0
  79. package/dist/utils/plugin-marketplace.d.ts +77 -0
  80. package/dist/utils/plugin-marketplace.js +191 -0
  81. package/dist/utils/presentation.d.ts +2 -0
  82. package/dist/utils/presentation.js +32 -0
  83. package/dist/utils/rate-limiter.d.ts +100 -0
  84. package/dist/utils/rate-limiter.js +256 -0
  85. package/dist/utils/scraper-cache.d.ts +115 -0
  86. package/dist/utils/scraper-cache.js +229 -0
  87. package/dist/utils/secure-config.d.ts +40 -0
  88. package/dist/utils/secure-config.js +195 -0
  89. package/dist/utils/streaming.d.ts +121 -0
  90. package/dist/utils/streaming.js +333 -0
  91. package/dist/utils/validation.d.ts +190 -0
  92. package/dist/utils/validation.js +209 -0
  93. package/dist/utils/version.d.ts +13 -0
  94. package/dist/utils/version.js +46 -0
  95. package/package.json +56 -0
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Get command - Retrieve legislation by ID
3
+ */
4
+ import { Command } from 'commander';
5
+ import ora from 'ora';
6
+ import { printWorkDetail, printVersionsTable, printJson, versionsToCsv } from '@output';
7
+ import { logger } from '@utils/logger';
8
+ import { validateWorkId, sanitizeInput } from '@utils/validation';
9
+ import { getGlobalRegistry } from '../providers/index.js';
10
+ import { toLegacyVersions, toLegacyWork } from '../providers/output-adapters.js';
11
+ export const getCommand = new Command()
12
+ .name('get')
13
+ .description('Get legislation by ID')
14
+ .argument('<id>', 'Work ID (e.g., act_public_1989_18)')
15
+ .option('--versions', 'Show version history')
16
+ .option('--format <format>', 'Output format (table, json, csv)', 'table')
17
+ .action(async (workId, options, command) => {
18
+ const globalOptions = command.parent ? command.parent.opts() : {};
19
+ const jurisdiction = globalOptions.jurisdiction || 'nz';
20
+ const spinner = ora(`Retrieving ${jurisdiction} legislation...`).start();
21
+ try {
22
+ // Get provider
23
+ const registry = getGlobalRegistry();
24
+ const provider = registry.get(jurisdiction);
25
+ if (!provider) {
26
+ spinner.stop();
27
+ console.error(`❌ Error: Unknown jurisdiction "${jurisdiction}"`);
28
+ process.exit(1);
29
+ }
30
+ // Sanitize and validate work ID
31
+ const sanitizedWorkId = sanitizeInput(workId);
32
+ // NZ-specific ID validation only if jurisdiction is nz
33
+ if (jurisdiction === 'nz') {
34
+ const validation = validateWorkId(sanitizedWorkId);
35
+ if (!validation.valid) {
36
+ spinner.stop();
37
+ logger.error('Work ID validation failed', undefined, {
38
+ workId,
39
+ errors: validation.errors,
40
+ });
41
+ console.error('❌ Invalid work ID format:');
42
+ validation.errors?.forEach(err => {
43
+ console.error(` - ${err.message}`);
44
+ });
45
+ console.error('\nExpected format: API work ID (e.g., act_public_1989_18)');
46
+ process.exit(3);
47
+ }
48
+ }
49
+ if (options.versions) {
50
+ // Get version history
51
+ const versions = await provider.getVersions(sanitizedWorkId);
52
+ const work = await provider.getWork(sanitizedWorkId);
53
+ const legacyVersions = toLegacyVersions(versions, work.type);
54
+ spinner.stop();
55
+ switch (options.format.toLowerCase()) {
56
+ case 'json':
57
+ printJson(versions);
58
+ break;
59
+ case 'csv':
60
+ console.log(versionsToCsv(legacyVersions));
61
+ break;
62
+ case 'table':
63
+ default:
64
+ printVersionsTable(legacyVersions);
65
+ break;
66
+ }
67
+ }
68
+ else {
69
+ // Get work details
70
+ const work = await provider.getWork(sanitizedWorkId);
71
+ const legacyWork = toLegacyWork(work);
72
+ spinner.stop();
73
+ switch (options.format.toLowerCase()) {
74
+ case 'json':
75
+ printJson(work);
76
+ break;
77
+ case 'csv':
78
+ console.log('Note: CSV format not ideal for single work. Use table or json.');
79
+ printWorkDetail(legacyWork);
80
+ break;
81
+ case 'table':
82
+ default:
83
+ printWorkDetail(legacyWork);
84
+ break;
85
+ }
86
+ }
87
+ }
88
+ catch (error) {
89
+ spinner.stop();
90
+ logger.error('Failed to retrieve legislation', error instanceof Error ? error : undefined, {
91
+ workId,
92
+ });
93
+ if (error instanceof Error) {
94
+ console.error(`❌ Error: ${error.message}`);
95
+ process.exit(1);
96
+ }
97
+ throw error;
98
+ }
99
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Interactive Help Command
3
+ * Provides menu-driven help navigation for ANZ Legislation CLI
4
+ */
5
+ import { Command } from 'commander';
6
+ /**
7
+ * Create interactive help command
8
+ */
9
+ export declare function createInteractiveHelpCommand(): Command;
10
+ /**
11
+ * Create contextual help command
12
+ */
13
+ export declare function createContextualHelpCommand(): Command;
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Interactive Help Command
3
+ * Provides menu-driven help navigation for ANZ Legislation CLI
4
+ */
5
+ import * as readline from 'readline';
6
+ import chalk from 'chalk';
7
+ import { Command } from 'commander';
8
+ const helpTopics = [
9
+ {
10
+ id: 'search',
11
+ title: 'Search Legislation',
12
+ description: 'Search for legislation works by keyword, type, or status',
13
+ examples: [
14
+ 'nzlegislation search --query "health"',
15
+ 'nzlegislation search --query "health" --type act --limit 10',
16
+ 'nzlegislation search --query "covid" --status in-force',
17
+ ],
18
+ relatedCommands: ['get', 'export'],
19
+ },
20
+ {
21
+ id: 'get',
22
+ title: 'Get Legislation Details',
23
+ description: 'Retrieve detailed information about a specific work',
24
+ examples: [
25
+ 'nzlegislation get "act_public_1989_18"',
26
+ 'nzlegislation get "act_public_1989_18" --versions',
27
+ 'nzlegislation get "act_public_1989_18" --format json',
28
+ ],
29
+ relatedCommands: ['search', 'cite'],
30
+ },
31
+ {
32
+ id: 'export',
33
+ title: 'Export Legislation',
34
+ description: 'Export search results to CSV, JSON, or NDJSON format',
35
+ examples: [
36
+ 'nzlegislation export --query "health" --output health.csv',
37
+ 'nzlegislation export --query "health" --output health.json --format json',
38
+ 'nzlegislation export --query "health" --output health.ndjson --format ndjson',
39
+ ],
40
+ relatedCommands: ['search'],
41
+ },
42
+ {
43
+ id: 'cite',
44
+ title: 'Generate Citations',
45
+ description: 'Generate citations in various academic styles',
46
+ examples: [
47
+ 'nzlegislation cite "act_public_1989_18" --style nzmj',
48
+ 'nzlegislation cite "act_public_1989_18" --style bibtex',
49
+ 'nzlegislation cite "act_public_1989_18" --style ris',
50
+ 'nzlegislation cite "act_public_1989_18" --style enw',
51
+ 'nzlegislation cite "act_public_1989_18" --style apa',
52
+ ],
53
+ relatedCommands: ['get'],
54
+ },
55
+ {
56
+ id: 'config',
57
+ title: 'Configuration',
58
+ description: 'Manage API key and CLI settings',
59
+ examples: [
60
+ 'nzlegislation config --key YOUR_API_KEY',
61
+ 'nzlegislation config --show',
62
+ 'nzlegislation config --clear',
63
+ ],
64
+ relatedCommands: [],
65
+ },
66
+ {
67
+ id: 'cache',
68
+ title: 'Cache Management',
69
+ description: 'View and manage API response cache',
70
+ examples: [
71
+ 'nzlegislation cache --status',
72
+ 'nzlegislation cache --clear',
73
+ 'nzlegislation cache --stats',
74
+ ],
75
+ relatedCommands: [],
76
+ },
77
+ {
78
+ id: 'troubleshooting',
79
+ title: 'Troubleshooting',
80
+ description: 'Common issues and solutions',
81
+ examples: [
82
+ chalk.yellow('Issue: API key not working'),
83
+ ' → Run: nzlegislation config --show',
84
+ ' → Run: nzlegislation config --key YOUR_API_KEY',
85
+ '',
86
+ chalk.yellow('Issue: Rate limit exceeded'),
87
+ ' → Wait 5 minutes and try again',
88
+ ' → Use --limit to reduce requests',
89
+ '',
90
+ chalk.yellow('Issue: Network error'),
91
+ ' → Check internet connection',
92
+ ' → Verify API URL: https://api.legislation.govt.nz',
93
+ ],
94
+ relatedCommands: ['config'],
95
+ },
96
+ ];
97
+ /**
98
+ * Create interactive help command
99
+ */
100
+ export function createInteractiveHelpCommand() {
101
+ const cmd = new Command();
102
+ cmd
103
+ .name('help-interactive')
104
+ .alias('help-i')
105
+ .description('Interactive help system with menu navigation')
106
+ .action(async () => {
107
+ console.log(chalk.blue.bold('\n📚 ANZ Legislation - Interactive Help\n'));
108
+ console.log('Use arrow keys to navigate, Enter to select, q to quit\n');
109
+ const rl = readline.createInterface({
110
+ input: process.stdin,
111
+ output: process.stdout,
112
+ });
113
+ let selectedIndex = 0;
114
+ const showMenu = () => {
115
+ console.clear();
116
+ console.log(chalk.blue.bold('\n📚 ANZ Legislation - Interactive Help\n'));
117
+ console.log('Select a topic:\n');
118
+ helpTopics.forEach((topic, index) => {
119
+ const isSelected = index === selectedIndex;
120
+ const symbol = isSelected ? chalk.green('❯') : ' ';
121
+ const color = isSelected ? chalk.green.bold : chalk.white;
122
+ console.log(`${symbol} ${color(topic.title)} - ${topic.description}`);
123
+ });
124
+ console.log('\n' + chalk.gray('q - Quit'));
125
+ console.log(chalk.gray('↑/↓ - Navigate | Enter - Select\n'));
126
+ };
127
+ const showTopic = (topic) => {
128
+ console.clear();
129
+ console.log(chalk.blue.bold(`\n📖 ${topic.title}\n`));
130
+ console.log(`${topic.description}\n`);
131
+ console.log(chalk.yellow.bold('Examples:'));
132
+ topic.examples.forEach(example => {
133
+ console.log(` ${chalk.cyan(example)}`);
134
+ });
135
+ if (topic.relatedCommands && topic.relatedCommands.length > 0) {
136
+ console.log('\n' + chalk.yellow.bold('Related Commands:'));
137
+ topic.relatedCommands.forEach(cmdName => {
138
+ console.log(` ${chalk.green(`nzlegislation ${cmdName} --help`)}`);
139
+ });
140
+ }
141
+ console.log('\n' + chalk.gray('Press Enter to return to menu...'));
142
+ };
143
+ // Handle keyboard input
144
+ const handleInput = async () => {
145
+ return new Promise(resolve => {
146
+ rl.once('data', (key) => {
147
+ const input = key.toString().toLowerCase();
148
+ if (input === 'q') {
149
+ console.log(chalk.green('\n👋 Goodbye!\n'));
150
+ rl.close();
151
+ process.exit(0);
152
+ }
153
+ if (input === '\r' || input === '\n') {
154
+ // Show selected topic
155
+ const topic = helpTopics[selectedIndex];
156
+ showTopic(topic);
157
+ // Wait for Enter to return
158
+ rl.once('data', () => {
159
+ showMenu();
160
+ void handleInput().then(resolve);
161
+ });
162
+ return;
163
+ }
164
+ if (input === '\x1b[A') {
165
+ // Up arrow
166
+ selectedIndex = Math.max(0, selectedIndex - 1);
167
+ }
168
+ else if (input === '\x1b[B') {
169
+ // Down arrow
170
+ selectedIndex = Math.min(helpTopics.length - 1, selectedIndex + 1);
171
+ }
172
+ showMenu();
173
+ void handleInput().then(resolve);
174
+ });
175
+ });
176
+ };
177
+ showMenu();
178
+ await handleInput();
179
+ });
180
+ return cmd;
181
+ }
182
+ /**
183
+ * Create contextual help command
184
+ */
185
+ export function createContextualHelpCommand() {
186
+ const cmd = new Command();
187
+ cmd
188
+ .name('help-context')
189
+ .description('Show contextual help for common scenarios')
190
+ .argument('[scenario]', 'Scenario: auth, rate-limit, network, export, cite')
191
+ .action((scenario) => {
192
+ if (!scenario) {
193
+ console.log(chalk.blue.bold('\n📚 Contextual Help\n'));
194
+ console.log('Available scenarios:\n');
195
+ console.log(' auth - Authentication and API key issues');
196
+ console.log(' rate-limit - Rate limiting and quotas');
197
+ console.log(' network - Network and connectivity issues');
198
+ console.log(' export - Export format and file issues');
199
+ console.log(' cite - Citation format and style issues');
200
+ console.log('\nUsage: nzlegislation help-context <scenario>\n');
201
+ return;
202
+ }
203
+ const scenarios = {
204
+ auth: [
205
+ chalk.yellow.bold('Authentication Issues'),
206
+ '',
207
+ chalk.green('Check API Key Configuration:'),
208
+ ' nzlegislation config --show',
209
+ '',
210
+ chalk.green('Set API Key:'),
211
+ ' nzlegislation config --key YOUR_API_KEY',
212
+ '',
213
+ chalk.green('Get API Key:'),
214
+ ' Visit: https://api.legislation.govt.nz/docs/',
215
+ '',
216
+ chalk.green('Environment Variable:'),
217
+ ' export NZ_LEGISLATION_API_KEY=your_key',
218
+ ],
219
+ 'rate-limit': [
220
+ chalk.yellow.bold('Rate Limiting'),
221
+ '',
222
+ chalk.green('Current Limits:'),
223
+ ' • 10,000 requests/day',
224
+ ' • 2,000 requests/5min (burst)',
225
+ '',
226
+ chalk.green('Solutions:'),
227
+ ' 1. Wait 5 minutes and retry',
228
+ ' 2. Use --limit to reduce batch size',
229
+ ' 3. Enable caching: nzlegislation cache --status',
230
+ ' 4. Export large datasets in chunks',
231
+ '',
232
+ chalk.green('Example:'),
233
+ ' nzlegislation search --query "health" --limit 50',
234
+ ],
235
+ network: [
236
+ chalk.yellow.bold('Network Issues'),
237
+ '',
238
+ chalk.green('Check Connection:'),
239
+ ' • Verify internet connection',
240
+ ' • Check API status: https://api.legislation.govt.nz',
241
+ ' • Try: curl https://api.legislation.govt.nz',
242
+ '',
243
+ chalk.green('Firewall/Proxy:'),
244
+ ' • Ensure HTTPS (port 443) is allowed',
245
+ ' • Configure proxy if needed',
246
+ '',
247
+ chalk.green('Timeout Issues:'),
248
+ ' • Increase timeout: export NZ_LEGISLATION_TIMEOUT=60000',
249
+ ],
250
+ export: [
251
+ chalk.yellow.bold('Export Issues'),
252
+ '',
253
+ chalk.green('Supported Formats:'),
254
+ ' • table (default) - Pretty terminal table',
255
+ ' • json - JSON format',
256
+ ' • csv - CSV format',
257
+ ' • ndjson - Newline-delimited JSON',
258
+ '',
259
+ chalk.green('Examples:'),
260
+ ' nzlegislation export --query "health" --output health.csv',
261
+ ' nzlegislation export --query "health" --format json',
262
+ '',
263
+ chalk.green('Troubleshooting:'),
264
+ ' • Check file path permissions',
265
+ ' • Ensure directory exists',
266
+ ' • Use absolute paths if needed',
267
+ ],
268
+ cite: [
269
+ chalk.yellow.bold('Citation Issues'),
270
+ '',
271
+ chalk.green('Supported Styles:'),
272
+ ' • nzmj - New Zealand Medical Journal',
273
+ ' • bibtex - BibTeX format',
274
+ ' • ris - RIS format (EndNote, Mendeley)',
275
+ ' • enw - EndNote import format',
276
+ ' • apa - APA style',
277
+ '',
278
+ chalk.green('Examples:'),
279
+ ' nzlegislation cite "act_public_1989_18" --style nzmj',
280
+ ' nzlegislation cite "act_public_1989_18" --style bibtex',
281
+ ' nzlegislation cite "act_public_1989_18" --style enw',
282
+ '',
283
+ chalk.green('Missing Data:'),
284
+ ' • Some works may lack metadata',
285
+ ' • Use --verbose to see available fields',
286
+ ],
287
+ };
288
+ const help = scenarios[scenario];
289
+ if (!help) {
290
+ console.log(chalk.red(`\nUnknown scenario: ${scenario}\n`));
291
+ console.log('Run without arguments to see available scenarios.\n');
292
+ process.exit(1);
293
+ }
294
+ console.log(help.join('\n'));
295
+ console.log();
296
+ });
297
+ return cmd;
298
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Search command - Search for legislation
3
+ */
4
+ import { Command } from 'commander';
5
+ export declare const searchCommand: Command;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Search command - Search for legislation
3
+ */
4
+ import { Command } from 'commander';
5
+ import ora from 'ora';
6
+ import { printTable, printJson, worksToCsv } from '@output';
7
+ import { logger } from '@utils/logger';
8
+ import { validateSearchParams, sanitizeInput } from '@utils/validation';
9
+ import { getGlobalRegistry } from '../providers/index.js';
10
+ import { toLegacySearchResults } from '../providers/output-adapters.js';
11
+ export const searchCommand = new Command()
12
+ .name('search')
13
+ .description('Search for legislation')
14
+ .requiredOption('-q, --query <text>', 'Search query')
15
+ .option('-t, --type <type>', 'Filter by type (act, bill, regulation, instrument)')
16
+ .option('-s, --status <status>', 'Filter by status (in-force, repealed, etc.)')
17
+ .option('--from <date>', 'Filter from date (YYYY-MM-DD)')
18
+ .option('--to <date>', 'Filter to date (YYYY-MM-DD)')
19
+ .option('-l, --limit <number>', 'Maximum results (default: 25, max: 100)', '25')
20
+ .option('-o, --offset <number>', 'Result offset for pagination', '0')
21
+ .option('--format <format>', 'Output format (table, json, csv)', 'table')
22
+ .action(async (options, command) => {
23
+ const globalOptions = command.parent ? command.parent.opts() : {};
24
+ const jurisdiction = globalOptions.jurisdiction || 'nz';
25
+ const spinner = ora(`Searching ${jurisdiction} legislation...`).start();
26
+ try {
27
+ // Get provider
28
+ const registry = getGlobalRegistry();
29
+ const provider = registry.get(jurisdiction);
30
+ if (!provider) {
31
+ spinner.stop();
32
+ console.error(`❌ Error: Unknown jurisdiction "${jurisdiction}"`);
33
+ process.exit(1);
34
+ }
35
+ // Sanitize inputs
36
+ const sanitizedOptions = {
37
+ ...options,
38
+ query: sanitizeInput(options.query),
39
+ type: options.type ? sanitizeInput(options.type) : undefined,
40
+ status: options.status ? sanitizeInput(options.status) : undefined,
41
+ from: options.from ? sanitizeInput(options.from) : undefined,
42
+ to: options.to ? sanitizeInput(options.to) : undefined,
43
+ };
44
+ // Validate parameters
45
+ const validation = validateSearchParams(sanitizedOptions);
46
+ if (!validation.valid) {
47
+ spinner.stop();
48
+ logger.error('Validation failed', undefined, { errors: validation.errors });
49
+ console.error('❌ Validation errors:');
50
+ validation.errors?.forEach(err => {
51
+ console.error(` - ${err.field}: ${err.message}`);
52
+ });
53
+ process.exit(3);
54
+ }
55
+ const validatedParams = validation.data;
56
+ const normalizedStatus = validatedParams.status === 'not-yet-in-force'
57
+ ? 'not-in-force'
58
+ : validatedParams.status === 'in-force' || validatedParams.status === 'repealed'
59
+ ? validatedParams.status
60
+ : undefined;
61
+ // Map Command-style WorkType to SearchParams WorkType
62
+ const searchParams = {
63
+ query: validatedParams.query,
64
+ type: validatedParams.type,
65
+ status: normalizedStatus,
66
+ from: validatedParams.from,
67
+ to: validatedParams.to,
68
+ limit: validatedParams.limit,
69
+ offset: validatedParams.offset,
70
+ };
71
+ const results = await provider.search(searchParams);
72
+ const legacyResults = toLegacySearchResults(results);
73
+ spinner.stop();
74
+ switch (validatedParams.format.toLowerCase()) {
75
+ case 'json':
76
+ printJson(results);
77
+ break;
78
+ case 'csv':
79
+ console.log(worksToCsv(legacyResults));
80
+ break;
81
+ case 'table':
82
+ default:
83
+ printTable(legacyResults);
84
+ break;
85
+ }
86
+ }
87
+ catch (error) {
88
+ spinner.stop();
89
+ logger.error('Search failed', error instanceof Error ? error : undefined, { options });
90
+ if (error instanceof Error) {
91
+ console.error(`❌ Error: ${error.message}`);
92
+ process.exit(1);
93
+ }
94
+ throw error;
95
+ }
96
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Stream Command - Stream large exports with minimal memory usage
3
+ */
4
+ import { Command } from 'commander';
5
+ export declare const streamCommand: Command;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Stream Command - Stream large exports with minimal memory usage
3
+ */
4
+ import { Command } from 'commander';
5
+ import ora from 'ora';
6
+ import { StreamExporter } from '@utils/streaming';
7
+ export const streamCommand = new Command()
8
+ .name('stream')
9
+ .description('Stream large exports with minimal memory usage')
10
+ .requiredOption('-q, --query <text>', 'Search query')
11
+ .requiredOption('-o, --output <file>', 'Output file path')
12
+ .option('-t, --type <type>', 'Filter by type (act, bill, regulation, instrument)')
13
+ .option('-s, --status <status>', 'Filter by status')
14
+ .option('--from <date>', 'Filter from date (YYYY-MM-DD)')
15
+ .option('--to <date>', 'Filter to date (YYYY-MM-DD)')
16
+ .option('-l, --limit <number>', 'Maximum results (default: unlimited)')
17
+ .option('-f, --format <format>', 'Output format (csv, json, ndjson)', 'csv')
18
+ .option('-b, --batch-size <number>', 'Results per batch', '100')
19
+ .option('-c, --concurrency <number>', 'API concurrency', '3')
20
+ .option('--no-metadata', 'Exclude metadata from output')
21
+ .action(async (options) => {
22
+ const spinner = ora('Starting stream export...').start();
23
+ try {
24
+ // Validate format
25
+ if (!['csv', 'json', 'ndjson'].includes(options.format)) {
26
+ throw new Error('Format must be csv, json, or ndjson');
27
+ }
28
+ // Parse options
29
+ const exportOptions = {
30
+ query: options.query,
31
+ outputPath: options.output,
32
+ format: options.format,
33
+ includeMetadata: !options.noMetadata,
34
+ batchSize: parseInt(options.batchSize, 10),
35
+ maxResults: options.limit ? parseInt(options.limit, 10) : undefined,
36
+ concurrency: parseInt(options.concurrency, 10),
37
+ };
38
+ // Create exporter
39
+ const exporter = new StreamExporter(exportOptions);
40
+ // Prepare search parameters
41
+ const searchParams = {
42
+ query: options.query,
43
+ type: options.type,
44
+ status: options.status,
45
+ from: options.from,
46
+ to: options.to,
47
+ };
48
+ spinner.succeed('Starting stream export');
49
+ console.log('\nExport Configuration:');
50
+ console.log('─'.repeat(50));
51
+ console.log(` Query: ${options.query}`);
52
+ console.log(` Output: ${options.output}`);
53
+ console.log(` Format: ${options.format.toUpperCase()}`);
54
+ console.log(` Batch Size: ${exportOptions.batchSize}`);
55
+ console.log(` Concurrency: ${exportOptions.concurrency}`);
56
+ console.log(` Max Results: ${exportOptions.maxResults || 'unlimited'}`);
57
+ console.log(` Metadata: ${exportOptions.includeMetadata ? 'Included' : 'Excluded'}`);
58
+ console.log('\nStreaming... (Press Ctrl+C to cancel)\n');
59
+ // Track progress
60
+ let lastPercent = 0;
61
+ const startTime = Date.now();
62
+ const result = await exporter.export(searchParams, progress => {
63
+ // Only show progress every 10%
64
+ if (progress.percent - lastPercent >= 10) {
65
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
66
+ const eta = progress.estimatedTimeRemaining
67
+ ? `${(progress.estimatedTimeRemaining / 1000).toFixed(0)}s`
68
+ : 'Calculating...';
69
+ console.log(` ${progress.percent}% | ${progress.processed} items | ${elapsed}s elapsed | ETA: ${eta}`);
70
+ lastPercent = progress.percent;
71
+ }
72
+ });
73
+ const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
74
+ spinner.succeed('Stream export complete');
75
+ console.log('\nExport Summary:');
76
+ console.log('─'.repeat(50));
77
+ console.log(` Items Exported: ${result.processed}`);
78
+ console.log(` File Size: ${(result.bytesWritten / 1024).toFixed(2)} KB`);
79
+ console.log(` Total Time: ${totalTime}s`);
80
+ console.log(` Output File: ${options.output}`);
81
+ console.log(` Format: ${options.format.toUpperCase()}`);
82
+ // Performance stats
83
+ const itemsPerSecond = ((result.processed / (Date.now() - startTime)) * 1000).toFixed(2);
84
+ console.log(` Throughput: ${itemsPerSecond} items/sec`);
85
+ // Memory usage
86
+ const memoryUsage = process.memoryUsage();
87
+ console.log(` Memory Used: ${(memoryUsage.heapUsed / 1024 / 1024).toFixed(2)} MB`);
88
+ }
89
+ catch (error) {
90
+ spinner.fail('Stream export failed');
91
+ console.error('\nError: Stream export failed.');
92
+ if (error instanceof Error && error.message.includes('EACCES')) {
93
+ console.log('\nHint: Check file permissions and ensure the output directory exists.');
94
+ }
95
+ else if (error instanceof Error && error.message.includes('ENOSPC')) {
96
+ console.log('\nHint: Disk space is full. Free up space and try again.');
97
+ }
98
+ process.exit(1);
99
+ }
100
+ });