@durable-streams/cli 0.1.7 → 0.1.8

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/dist/index.cjs CHANGED
@@ -23,6 +23,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  }) : target, mod));
24
24
 
25
25
  //#endregion
26
+ const node_http = __toESM(require("node:http"));
26
27
  const node_path = __toESM(require("node:path"));
27
28
  const node_process = __toESM(require("node:process"));
28
29
  const node_url = __toESM(require("node:url"));
@@ -89,43 +90,146 @@ function parseWriteArgs(args) {
89
90
  };
90
91
  }
91
92
 
93
+ //#endregion
94
+ //#region src/validation.ts
95
+ /**
96
+ * Validate a URL string.
97
+ * Must be a valid HTTP or HTTPS URL.
98
+ */
99
+ function validateUrl(url) {
100
+ if (!url || !url.trim()) return {
101
+ valid: false,
102
+ error: `URL cannot be empty`
103
+ };
104
+ let parsed;
105
+ try {
106
+ parsed = new URL(url);
107
+ } catch {
108
+ return {
109
+ valid: false,
110
+ error: `Invalid URL format: "${url}"\n Expected format: http://host:port or https://host:port`
111
+ };
112
+ }
113
+ if (parsed.protocol !== `http:` && parsed.protocol !== `https:`) return {
114
+ valid: false,
115
+ error: `Invalid URL protocol: "${parsed.protocol}"\n Only http:// and https:// are supported`
116
+ };
117
+ if (!parsed.hostname) return {
118
+ valid: false,
119
+ error: `URL must include a hostname: "${url}"`
120
+ };
121
+ return { valid: true };
122
+ }
123
+ /**
124
+ * Normalize a base URL by removing trailing slashes.
125
+ * This prevents double-slashes when appending paths (e.g., "http://host/" + "/v1/...").
126
+ */
127
+ function normalizeBaseUrl(url) {
128
+ return url.replace(/\/+$/, ``);
129
+ }
130
+ /**
131
+ * Validate an authorization header value.
132
+ * Returns valid=true for any non-empty value, but includes a warning
133
+ * if the value doesn't match common auth schemes (Bearer, Basic, ApiKey, Token).
134
+ */
135
+ function validateAuth(auth) {
136
+ if (!auth || !auth.trim()) return {
137
+ valid: false,
138
+ error: `Authorization value cannot be empty`
139
+ };
140
+ const trimmed = auth.trim();
141
+ const lowerTrimmed = trimmed.toLowerCase();
142
+ const hasScheme = [
143
+ `bearer `,
144
+ `basic `,
145
+ `apikey `,
146
+ `token `
147
+ ].some((scheme) => lowerTrimmed.startsWith(scheme));
148
+ if (!hasScheme && !trimmed.includes(` `)) return {
149
+ valid: true,
150
+ warning: `Warning: Authorization value doesn't match common formats.\n Expected: "Bearer <token>", "Basic <credentials>", or "ApiKey <key>"`
151
+ };
152
+ return { valid: true };
153
+ }
154
+ /**
155
+ * Validate a stream ID.
156
+ * Must be 1-256 characters containing only: letters, numbers, underscores,
157
+ * hyphens, dots, and colons (URL-safe characters).
158
+ */
159
+ function validateStreamId(streamId) {
160
+ if (!streamId || !streamId.trim()) return {
161
+ valid: false,
162
+ error: `Stream ID cannot be empty`
163
+ };
164
+ const validPattern = /^[a-zA-Z0-9_\-.:]+$/;
165
+ if (!validPattern.test(streamId)) return {
166
+ valid: false,
167
+ error: `Invalid stream ID: "${streamId}"\n Stream IDs can only contain letters, numbers, underscores, hyphens, dots, and colons`
168
+ };
169
+ if (streamId.length > 256) return {
170
+ valid: false,
171
+ error: `Stream ID too long (${streamId.length} chars)\n Maximum length is 256 characters`
172
+ };
173
+ return { valid: true };
174
+ }
175
+
92
176
  //#endregion
93
177
  //#region src/index.ts
94
178
  const STREAM_URL = process.env.STREAM_URL || `http://localhost:4437`;
95
179
  const STREAM_AUTH = process.env.STREAM_AUTH;
96
180
  /**
97
- * Parse global options (like --auth) from args.
98
- * Falls back to STREAM_AUTH env var if --auth flag not provided.
99
- * Returns the parsed options and remaining args.
181
+ * Parse global options (--url, --auth) from args.
182
+ * Falls back to STREAM_URL/STREAM_AUTH env vars when flags not provided.
183
+ * Returns the parsed options, remaining args, and any warnings.
100
184
  */
101
185
  function parseGlobalOptions(args) {
102
186
  const options = {};
103
187
  const remainingArgs = [];
188
+ const warnings = [];
104
189
  for (let i = 0; i < args.length; i++) {
105
190
  const arg = args[i];
106
- if (arg === `--auth`) {
191
+ if (arg === `--url`) {
192
+ const value = args[i + 1];
193
+ if (!value || value.startsWith(`--`)) throw new Error(`--url requires a value\n Example: --url "http://localhost:4437"`);
194
+ const urlValidation = validateUrl(value);
195
+ if (!urlValidation.valid) throw new Error(urlValidation.error);
196
+ options.url = normalizeBaseUrl(value);
197
+ i++;
198
+ } else if (arg === `--auth`) {
107
199
  const value = args[i + 1];
108
- if (!value || value.startsWith(`--`)) throw new Error(`--auth requires a value (e.g., --auth "Bearer token")`);
109
- if (!value.trim()) throw new Error(`--auth value cannot be empty or whitespace`);
200
+ if (!value || value.startsWith(`--`)) throw new Error(`--auth requires a value\n Example: --auth "Bearer my-token"`);
201
+ const authValidation = validateAuth(value);
202
+ if (!authValidation.valid) throw new Error(authValidation.error);
203
+ if (authValidation.warning) warnings.push(authValidation.warning);
110
204
  options.auth = value;
111
205
  i++;
112
206
  } else remainingArgs.push(arg);
113
207
  }
208
+ if (!options.url) {
209
+ const urlValidation = validateUrl(STREAM_URL);
210
+ if (!urlValidation.valid) throw new Error(`Invalid STREAM_URL environment variable: ${urlValidation.error}`);
211
+ options.url = normalizeBaseUrl(STREAM_URL);
212
+ }
114
213
  if (!options.auth && STREAM_AUTH) {
115
- if (!STREAM_AUTH.trim()) throw new Error(`STREAM_AUTH environment variable cannot be empty or whitespace`);
214
+ const authValidation = validateAuth(STREAM_AUTH);
215
+ if (!authValidation.valid) throw new Error(`Invalid STREAM_AUTH environment variable: ${authValidation.error}`);
216
+ if (authValidation.warning) warnings.push(authValidation.warning);
116
217
  options.auth = STREAM_AUTH;
117
218
  }
118
219
  return {
119
220
  options,
120
- remainingArgs
221
+ remainingArgs,
222
+ warnings
121
223
  };
122
224
  }
225
+ function getErrorMessage(error) {
226
+ return error instanceof Error ? error.message : String(error);
227
+ }
123
228
  function buildHeaders(options) {
124
- if (options.auth) return { Authorization: options.auth };
125
- return {};
229
+ return options.auth ? { Authorization: options.auth } : {};
126
230
  }
127
- function printUsage() {
128
- console.error(`
231
+ function getUsageText() {
232
+ return `
129
233
  Usage:
130
234
  durable-stream create <stream_id> Create a new stream
131
235
  durable-stream write <stream_id> <content> Write content to a stream
@@ -134,7 +238,9 @@ Usage:
134
238
  durable-stream delete <stream_id> Delete a stream
135
239
 
136
240
  Global Options:
241
+ --url <url> Stream server URL (overrides STREAM_URL env var)
137
242
  --auth <value> Authorization header value (e.g., "Bearer my-token")
243
+ --help, -h Show this help message
138
244
 
139
245
  Write Options:
140
246
  --content-type <type> Content-Type for the message (default: application/octet-stream)
@@ -144,191 +250,255 @@ Write Options:
144
250
  Environment Variables:
145
251
  STREAM_URL Base URL of the stream server (default: http://localhost:4437)
146
252
  STREAM_AUTH Authorization header value (overridden by --auth flag)
147
- `);
253
+ `;
148
254
  }
149
- async function createStream(streamId, headers) {
150
- const url = `${STREAM_URL}/v1/stream/${streamId}`;
255
+ function printUsage({ to = `stderr` } = {}) {
256
+ const out = to === `stderr` ? node_process.stderr : node_process.stdout;
257
+ out.write(getUsageText());
258
+ }
259
+ async function createStream(baseUrl, streamId, headers) {
260
+ const url = `${baseUrl}/v1/stream/${streamId}`;
151
261
  try {
152
262
  await __durable_streams_client.DurableStream.create({
153
263
  url,
154
264
  headers,
155
265
  contentType: `application/octet-stream`
156
266
  });
157
- console.log(`Created stream: ${streamId}`);
267
+ console.log(`Stream created successfully: "${streamId}"`);
268
+ console.log(` URL: ${url}`);
158
269
  } catch (error) {
159
- const message = error instanceof Error ? error.message : String(error);
160
- node_process.stderr.write(`Error creating stream: ${message}\n`);
270
+ node_process.stderr.write(`Failed to create stream "${streamId}"\n`);
271
+ node_process.stderr.write(` ${formatErrorMessage(getErrorMessage(error))}\n`);
161
272
  process.exit(1);
162
273
  }
163
274
  }
164
275
  /**
165
- * Append JSON data to a stream with one-level array flattening.
276
+ * Format error messages from the server/client for better readability.
166
277
  */
167
- async function appendJson(stream, parsed) {
168
- let count = 0;
169
- for (const item of flattenJsonForAppend(parsed)) {
170
- await stream.append(item);
171
- count++;
278
+ function formatErrorMessage(message) {
279
+ const httpMatch = message.match(/HTTP Error (\d+)/);
280
+ const statusCode = httpMatch?.[1];
281
+ if (statusCode) {
282
+ const status = parseInt(statusCode, 10);
283
+ const statusText = getHttpStatusText(status);
284
+ return message.replace(/HTTP Error \d+/, `${statusText} (${status})`);
172
285
  }
173
- return count;
286
+ return message;
287
+ }
288
+ function getHttpStatusText(status) {
289
+ return node_http.STATUS_CODES[status] ?? `HTTP Error`;
290
+ }
291
+ /**
292
+ * Append JSON data to a stream using batch semantics.
293
+ * Arrays are flattened one level (each element becomes a separate message).
294
+ * Non-array values are written as a single message.
295
+ * Returns the number of messages written.
296
+ */
297
+ async function appendJsonBatch(stream, parsed) {
298
+ const items = [...flattenJsonForAppend(parsed)];
299
+ for (const item of items) await stream.append(JSON.stringify(item));
300
+ return items.length;
301
+ }
302
+ /**
303
+ * Read all data from stdin into a Buffer.
304
+ */
305
+ async function readStdin() {
306
+ const chunks = [];
307
+ node_process.stdin.on(`data`, (chunk) => {
308
+ chunks.push(chunk);
309
+ });
310
+ await new Promise((resolve, reject) => {
311
+ node_process.stdin.on(`end`, resolve);
312
+ node_process.stdin.on(`error`, (err) => {
313
+ reject(new Error(`Failed to read from stdin: ${err.message}`));
314
+ });
315
+ });
316
+ return Buffer.concat(chunks);
174
317
  }
175
- async function writeStream(streamId, contentType, batchJson, headers, content) {
176
- const url = `${STREAM_URL}/v1/stream/${streamId}`;
318
+ /**
319
+ * Process escape sequences in content string.
320
+ */
321
+ function processEscapeSequences(content) {
322
+ return content.replace(/\\n/g, `\n`).replace(/\\t/g, `\t`).replace(/\\r/g, `\r`).replace(/\\\\/g, `\\`);
323
+ }
324
+ async function writeStream(baseUrl, streamId, contentType, batchJson, headers, content) {
325
+ const url = `${baseUrl}/v1/stream/${streamId}`;
177
326
  const isJson = isJsonContentType(contentType);
327
+ let data;
328
+ let source;
329
+ if (content) {
330
+ data = processEscapeSequences(content);
331
+ source = `argument`;
332
+ } else {
333
+ data = await readStdin();
334
+ source = `stdin`;
335
+ if (data.length === 0) {
336
+ node_process.stderr.write(`No data received from stdin\n`);
337
+ process.exit(1);
338
+ }
339
+ }
178
340
  try {
179
341
  const stream = new __durable_streams_client.DurableStream({
180
342
  url,
181
343
  headers,
182
344
  contentType
183
345
  });
184
- if (content) {
185
- const processedContent = content.replace(/\\n/g, `\n`).replace(/\\t/g, `\t`).replace(/\\r/g, `\r`).replace(/\\\\/g, `\\`);
186
- if (isJson) {
187
- const parsed = JSON.parse(processedContent);
188
- if (batchJson) {
189
- const count = await appendJson(stream, parsed);
190
- console.log(`Wrote ${count} message(s) to ${streamId}`);
346
+ if (isJson) {
347
+ const jsonString = typeof data === `string` ? data : data.toString(`utf8`);
348
+ let parsed;
349
+ try {
350
+ parsed = JSON.parse(jsonString);
351
+ } catch (parseError) {
352
+ const parseMessage = parseError instanceof SyntaxError ? parseError.message : `Unknown parsing error`;
353
+ if (source === `argument`) {
354
+ const preview = jsonString.slice(0, 100);
355
+ const ellipsis = jsonString.length > 100 ? `...` : ``;
356
+ node_process.stderr.write(`Failed to parse JSON content\n`);
357
+ node_process.stderr.write(` ${parseMessage}\n`);
358
+ node_process.stderr.write(` Input: ${preview}${ellipsis}\n`);
191
359
  } else {
192
- await stream.append(parsed);
193
- console.log(`Wrote 1 message to ${streamId}`);
360
+ node_process.stderr.write(`Failed to parse JSON from stdin\n`);
361
+ node_process.stderr.write(` ${parseMessage}\n`);
194
362
  }
195
- } else {
196
- await stream.append(processedContent);
197
- console.log(`Wrote ${processedContent.length} bytes to ${streamId}`);
363
+ process.exit(1);
198
364
  }
199
- } else {
200
- const chunks = [];
201
- node_process.stdin.on(`data`, (chunk) => {
202
- chunks.push(chunk);
203
- });
204
- await new Promise((resolve, reject) => {
205
- node_process.stdin.on(`end`, resolve);
206
- node_process.stdin.on(`error`, reject);
207
- });
208
- const data = Buffer.concat(chunks);
209
- if (isJson) {
210
- const parsed = JSON.parse(data.toString(`utf8`));
211
- if (batchJson) {
212
- const count = await appendJson(stream, parsed);
213
- console.log(`Wrote ${count} message(s) to ${streamId}`);
214
- } else {
215
- await stream.append(parsed);
216
- console.log(`Wrote 1 message to ${streamId}`);
217
- }
365
+ if (batchJson) {
366
+ const count = await appendJsonBatch(stream, parsed);
367
+ console.log(`Wrote ${count} message${count !== 1 ? `s` : ``} to stream "${streamId}"`);
218
368
  } else {
219
- await stream.append(data);
220
- console.log(`Wrote ${data.length} bytes to ${streamId}`);
369
+ await stream.append(JSON.stringify(parsed));
370
+ console.log(`Wrote 1 JSON message to stream "${streamId}"`);
221
371
  }
372
+ } else {
373
+ await stream.append(data);
374
+ const byteCount = typeof data === `string` ? Buffer.byteLength(data, `utf8`) : data.length;
375
+ console.log(`Wrote ${formatBytes(byteCount)} to stream "${streamId}"`);
222
376
  }
223
377
  } catch (error) {
224
- const message = error instanceof Error ? error.message : String(error);
225
- node_process.stderr.write(`Error writing to stream: ${message}\n`);
378
+ node_process.stderr.write(`Failed to write to stream "${streamId}"\n`);
379
+ node_process.stderr.write(` ${formatErrorMessage(getErrorMessage(error))}\n`);
226
380
  process.exit(1);
227
381
  }
228
382
  }
229
- async function readStream(streamId, headers) {
230
- const url = `${STREAM_URL}/v1/stream/${streamId}`;
383
+ function formatBytes(bytes) {
384
+ if (bytes === 1) return `1 byte`;
385
+ if (bytes < 1024) return `${bytes} bytes`;
386
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
387
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
388
+ }
389
+ async function readStream(baseUrl, streamId, headers) {
390
+ const url = `${baseUrl}/v1/stream/${streamId}`;
231
391
  try {
232
392
  const stream = new __durable_streams_client.DurableStream({
233
393
  url,
234
394
  headers
235
395
  });
236
- const res = await stream.stream({ live: `auto` });
396
+ const res = await stream.stream({ live: true });
237
397
  for await (const chunk of res.bodyStream()) if (chunk.length > 0) node_process.stdout.write(chunk);
238
398
  } catch (error) {
239
- const message = error instanceof Error ? error.message : String(error);
240
- node_process.stderr.write(`Error reading stream: ${message}\n`);
399
+ node_process.stderr.write(`Failed to read stream "${streamId}"\n`);
400
+ node_process.stderr.write(` ${formatErrorMessage(getErrorMessage(error))}\n`);
241
401
  process.exit(1);
242
402
  }
243
403
  }
244
- async function deleteStream(streamId, headers) {
245
- const url = `${STREAM_URL}/v1/stream/${streamId}`;
404
+ async function deleteStream(baseUrl, streamId, headers) {
405
+ const url = `${baseUrl}/v1/stream/${streamId}`;
246
406
  try {
247
407
  const stream = new __durable_streams_client.DurableStream({
248
408
  url,
249
409
  headers
250
410
  });
251
411
  await stream.delete();
252
- console.log(`Deleted stream: ${streamId}`);
412
+ console.log(`Stream deleted successfully: "${streamId}"`);
253
413
  } catch (error) {
254
- const message = error instanceof Error ? error.message : String(error);
255
- node_process.stderr.write(`Error deleting stream: ${message}\n`);
414
+ node_process.stderr.write(`Failed to delete stream "${streamId}"\n`);
415
+ node_process.stderr.write(` ${formatErrorMessage(getErrorMessage(error))}\n`);
256
416
  process.exit(1);
257
417
  }
258
418
  }
259
419
  async function main() {
420
+ const rawArgs = process.argv.slice(2);
421
+ if (rawArgs.includes(`--help`) || rawArgs.includes(`-h`)) {
422
+ printUsage({ to: `stdout` });
423
+ process.exit(0);
424
+ }
260
425
  let options;
261
426
  let args;
427
+ let warnings;
262
428
  try {
263
- const parsed = parseGlobalOptions(process.argv.slice(2));
429
+ const parsed = parseGlobalOptions(rawArgs);
264
430
  options = parsed.options;
265
431
  args = parsed.remainingArgs;
432
+ warnings = parsed.warnings;
266
433
  } catch (error) {
267
- const message = error instanceof Error ? error.message : String(error);
268
- node_process.stderr.write(`Error: ${message}\n`);
434
+ node_process.stderr.write(`Error: ${getErrorMessage(error)}\n`);
269
435
  process.exit(1);
270
436
  }
437
+ for (const warning of warnings) node_process.stderr.write(`${warning}\n`);
271
438
  const headers = buildHeaders(options);
272
439
  if (args.length < 1) {
440
+ node_process.stderr.write(`Error: No command specified\n`);
273
441
  printUsage();
274
442
  process.exit(1);
275
443
  }
276
444
  const command = args[0];
445
+ function getStreamId() {
446
+ if (args.length < 2) {
447
+ node_process.stderr.write(`Error: Missing stream_id\n`);
448
+ node_process.stderr.write(` Usage: durable-stream ${command} <stream_id>\n`);
449
+ process.exit(1);
450
+ }
451
+ const streamId = args[1];
452
+ const validation = validateStreamId(streamId);
453
+ if (!validation.valid) {
454
+ node_process.stderr.write(`Error: ${validation.error}\n`);
455
+ process.exit(1);
456
+ }
457
+ return streamId;
458
+ }
277
459
  switch (command) {
278
460
  case `create`: {
279
- if (args.length < 2) {
280
- node_process.stderr.write(`Error: stream_id required\n`);
281
- printUsage();
282
- process.exit(1);
283
- }
284
- await createStream(args[1], headers);
461
+ const streamId = getStreamId();
462
+ await createStream(options.url, streamId, headers);
285
463
  break;
286
464
  }
287
465
  case `write`: {
288
- if (args.length < 2) {
289
- node_process.stderr.write(`Error: stream_id required\n`);
290
- printUsage();
291
- process.exit(1);
292
- }
293
- const streamId = args[1];
466
+ const streamId = getStreamId();
294
467
  let parsed;
295
468
  try {
296
469
  parsed = parseWriteArgs(args.slice(2));
297
470
  } catch (error) {
298
- const message = error instanceof Error ? error.message : String(error);
299
- node_process.stderr.write(`Error: ${message}\n`);
471
+ node_process.stderr.write(`Error: ${getErrorMessage(error)}\n`);
300
472
  process.exit(1);
301
473
  }
302
- if (!node_process.stdin.isTTY) await writeStream(streamId, parsed.contentType, parsed.batchJson, headers);
303
- else if (parsed.content) await writeStream(streamId, parsed.contentType, parsed.batchJson, headers, parsed.content);
474
+ const hasContent = parsed.content || !node_process.stdin.isTTY;
475
+ if (hasContent) await writeStream(options.url, streamId, parsed.contentType, parsed.batchJson, headers, parsed.content || void 0);
304
476
  else {
305
- node_process.stderr.write(`Error: content required (provide as argument or pipe to stdin)\n`);
306
- printUsage();
477
+ node_process.stderr.write(`Error: No content provided\n`);
478
+ node_process.stderr.write(` Provide content as an argument or pipe from stdin:\n`);
479
+ node_process.stderr.write(` durable-stream write ${streamId} "your content here"\n`);
480
+ node_process.stderr.write(` echo "content" | durable-stream write ${streamId}\n`);
307
481
  process.exit(1);
308
482
  }
309
483
  break;
310
484
  }
311
485
  case `read`: {
312
- if (args.length < 2) {
313
- node_process.stderr.write(`Error: stream_id required\n`);
314
- printUsage();
315
- process.exit(1);
316
- }
317
- await readStream(args[1], headers);
486
+ const streamId = getStreamId();
487
+ await readStream(options.url, streamId, headers);
318
488
  break;
319
489
  }
320
490
  case `delete`: {
321
- if (args.length < 2) {
322
- node_process.stderr.write(`Error: stream_id required\n`);
323
- printUsage();
324
- process.exit(1);
325
- }
326
- await deleteStream(args[1], headers);
491
+ const streamId = getStreamId();
492
+ await deleteStream(options.url, streamId, headers);
327
493
  break;
328
494
  }
329
495
  default:
330
- node_process.stderr.write(`Error: unknown command '${command}'\n`);
331
- printUsage();
496
+ if (command?.startsWith(`-`)) node_process.stderr.write(`Error: Unknown option "${command}"\n`);
497
+ else {
498
+ node_process.stderr.write(`Error: Unknown command "${command}"\n`);
499
+ node_process.stderr.write(` Available commands: create, write, read, delete\n`);
500
+ }
501
+ node_process.stderr.write(` Run "durable-stream --help" for usage information\n`);
332
502
  process.exit(1);
333
503
  }
334
504
  }
@@ -339,14 +509,18 @@ function isMainModule() {
339
509
  return scriptPath === modulePath;
340
510
  }
341
511
  if (isMainModule()) main().catch((error) => {
342
- const message = error instanceof Error ? error.message : String(error);
343
- node_process.stderr.write(`Fatal error: ${message}\n`);
512
+ node_process.stderr.write(`Fatal error: ${getErrorMessage(error)}\n`);
344
513
  process.exit(1);
345
514
  });
346
515
 
347
516
  //#endregion
348
517
  exports.buildHeaders = buildHeaders
349
518
  exports.flattenJsonForAppend = flattenJsonForAppend
519
+ exports.getUsageText = getUsageText
350
520
  exports.isJsonContentType = isJsonContentType
521
+ exports.normalizeBaseUrl = normalizeBaseUrl
351
522
  exports.parseGlobalOptions = parseGlobalOptions
352
- exports.parseWriteArgs = parseWriteArgs
523
+ exports.parseWriteArgs = parseWriteArgs
524
+ exports.validateAuth = validateAuth
525
+ exports.validateStreamId = validateStreamId
526
+ exports.validateUrl = validateUrl
package/dist/index.d.cts CHANGED
@@ -29,21 +29,57 @@ interface ParsedWriteArgs {
29
29
  */
30
30
  declare function parseWriteArgs(args: Array<string>): ParsedWriteArgs;
31
31
 
32
+ //#endregion
33
+ //#region src/validation.d.ts
34
+ /**
35
+ * Validation utilities for CLI options with helpful error messages.
36
+ */
37
+ interface ValidationResult {
38
+ valid: boolean;
39
+ error?: string;
40
+ warning?: string;
41
+ }
42
+ /**
43
+ * Validate a URL string.
44
+ * Must be a valid HTTP or HTTPS URL.
45
+ */
46
+ declare function validateUrl(url: string): ValidationResult;
47
+ /**
48
+ * Normalize a base URL by removing trailing slashes.
49
+ * This prevents double-slashes when appending paths (e.g., "http://host/" + "/v1/...").
50
+ */
51
+ declare function normalizeBaseUrl(url: string): string;
52
+ /**
53
+ * Validate an authorization header value.
54
+ * Returns valid=true for any non-empty value, but includes a warning
55
+ * if the value doesn't match common auth schemes (Bearer, Basic, ApiKey, Token).
56
+ */
57
+ declare function validateAuth(auth: string): ValidationResult;
58
+ /**
59
+ * Validate a stream ID.
60
+ * Must be 1-256 characters containing only: letters, numbers, underscores,
61
+ * hyphens, dots, and colons (URL-safe characters).
62
+ */
63
+ declare function validateStreamId(streamId: string): ValidationResult;
64
+
32
65
  //#endregion
33
66
  //#region src/index.d.ts
34
67
  interface GlobalOptions {
68
+ url?: string;
35
69
  auth?: string;
36
70
  }
37
71
  /**
38
- * Parse global options (like --auth) from args.
39
- * Falls back to STREAM_AUTH env var if --auth flag not provided.
40
- * Returns the parsed options and remaining args.
72
+ * Parse global options (--url, --auth) from args.
73
+ * Falls back to STREAM_URL/STREAM_AUTH env vars when flags not provided.
74
+ * Returns the parsed options, remaining args, and any warnings.
41
75
  */
42
76
  declare function parseGlobalOptions(args: Array<string>): {
43
77
  options: GlobalOptions;
44
78
  remainingArgs: Array<string>;
79
+ warnings: Array<string>;
45
80
  };
46
81
  declare function buildHeaders(options: GlobalOptions): Record<string, string>;
82
+ declare function getUsageText(): string;
47
83
 
48
84
  //#endregion
49
- export { GlobalOptions, ParsedWriteArgs, buildHeaders, flattenJsonForAppend, isJsonContentType, parseGlobalOptions, parseWriteArgs };
85
+ export { GlobalOptions, ParsedWriteArgs, buildHeaders, flattenJsonForAppend, getUsageText, isJsonContentType, normalizeBaseUrl, parseGlobalOptions, parseWriteArgs, validateAuth, validateStreamId, validateUrl };
package/dist/index.d.ts CHANGED
@@ -29,21 +29,57 @@ interface ParsedWriteArgs {
29
29
  */
30
30
  declare function parseWriteArgs(args: Array<string>): ParsedWriteArgs;
31
31
 
32
+ //#endregion
33
+ //#region src/validation.d.ts
34
+ /**
35
+ * Validation utilities for CLI options with helpful error messages.
36
+ */
37
+ interface ValidationResult {
38
+ valid: boolean;
39
+ error?: string;
40
+ warning?: string;
41
+ }
42
+ /**
43
+ * Validate a URL string.
44
+ * Must be a valid HTTP or HTTPS URL.
45
+ */
46
+ declare function validateUrl(url: string): ValidationResult;
47
+ /**
48
+ * Normalize a base URL by removing trailing slashes.
49
+ * This prevents double-slashes when appending paths (e.g., "http://host/" + "/v1/...").
50
+ */
51
+ declare function normalizeBaseUrl(url: string): string;
52
+ /**
53
+ * Validate an authorization header value.
54
+ * Returns valid=true for any non-empty value, but includes a warning
55
+ * if the value doesn't match common auth schemes (Bearer, Basic, ApiKey, Token).
56
+ */
57
+ declare function validateAuth(auth: string): ValidationResult;
58
+ /**
59
+ * Validate a stream ID.
60
+ * Must be 1-256 characters containing only: letters, numbers, underscores,
61
+ * hyphens, dots, and colons (URL-safe characters).
62
+ */
63
+ declare function validateStreamId(streamId: string): ValidationResult;
64
+
32
65
  //#endregion
33
66
  //#region src/index.d.ts
34
67
  interface GlobalOptions {
68
+ url?: string;
35
69
  auth?: string;
36
70
  }
37
71
  /**
38
- * Parse global options (like --auth) from args.
39
- * Falls back to STREAM_AUTH env var if --auth flag not provided.
40
- * Returns the parsed options and remaining args.
72
+ * Parse global options (--url, --auth) from args.
73
+ * Falls back to STREAM_URL/STREAM_AUTH env vars when flags not provided.
74
+ * Returns the parsed options, remaining args, and any warnings.
41
75
  */
42
76
  declare function parseGlobalOptions(args: Array<string>): {
43
77
  options: GlobalOptions;
44
78
  remainingArgs: Array<string>;
79
+ warnings: Array<string>;
45
80
  };
46
81
  declare function buildHeaders(options: GlobalOptions): Record<string, string>;
82
+ declare function getUsageText(): string;
47
83
 
48
84
  //#endregion
49
- export { GlobalOptions, ParsedWriteArgs, buildHeaders, flattenJsonForAppend, isJsonContentType, parseGlobalOptions, parseWriteArgs };
85
+ export { GlobalOptions, ParsedWriteArgs, buildHeaders, flattenJsonForAppend, getUsageText, isJsonContentType, normalizeBaseUrl, parseGlobalOptions, parseWriteArgs, validateAuth, validateStreamId, validateUrl };
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { STATUS_CODES } from "node:http";
2
3
  import { resolve } from "node:path";
3
4
  import { stderr, stdin, stdout } from "node:process";
4
5
  import { fileURLToPath } from "node:url";
@@ -65,43 +66,146 @@ function parseWriteArgs(args) {
65
66
  };
66
67
  }
67
68
 
69
+ //#endregion
70
+ //#region src/validation.ts
71
+ /**
72
+ * Validate a URL string.
73
+ * Must be a valid HTTP or HTTPS URL.
74
+ */
75
+ function validateUrl(url) {
76
+ if (!url || !url.trim()) return {
77
+ valid: false,
78
+ error: `URL cannot be empty`
79
+ };
80
+ let parsed;
81
+ try {
82
+ parsed = new URL(url);
83
+ } catch {
84
+ return {
85
+ valid: false,
86
+ error: `Invalid URL format: "${url}"\n Expected format: http://host:port or https://host:port`
87
+ };
88
+ }
89
+ if (parsed.protocol !== `http:` && parsed.protocol !== `https:`) return {
90
+ valid: false,
91
+ error: `Invalid URL protocol: "${parsed.protocol}"\n Only http:// and https:// are supported`
92
+ };
93
+ if (!parsed.hostname) return {
94
+ valid: false,
95
+ error: `URL must include a hostname: "${url}"`
96
+ };
97
+ return { valid: true };
98
+ }
99
+ /**
100
+ * Normalize a base URL by removing trailing slashes.
101
+ * This prevents double-slashes when appending paths (e.g., "http://host/" + "/v1/...").
102
+ */
103
+ function normalizeBaseUrl(url) {
104
+ return url.replace(/\/+$/, ``);
105
+ }
106
+ /**
107
+ * Validate an authorization header value.
108
+ * Returns valid=true for any non-empty value, but includes a warning
109
+ * if the value doesn't match common auth schemes (Bearer, Basic, ApiKey, Token).
110
+ */
111
+ function validateAuth(auth) {
112
+ if (!auth || !auth.trim()) return {
113
+ valid: false,
114
+ error: `Authorization value cannot be empty`
115
+ };
116
+ const trimmed = auth.trim();
117
+ const lowerTrimmed = trimmed.toLowerCase();
118
+ const hasScheme = [
119
+ `bearer `,
120
+ `basic `,
121
+ `apikey `,
122
+ `token `
123
+ ].some((scheme) => lowerTrimmed.startsWith(scheme));
124
+ if (!hasScheme && !trimmed.includes(` `)) return {
125
+ valid: true,
126
+ warning: `Warning: Authorization value doesn't match common formats.\n Expected: "Bearer <token>", "Basic <credentials>", or "ApiKey <key>"`
127
+ };
128
+ return { valid: true };
129
+ }
130
+ /**
131
+ * Validate a stream ID.
132
+ * Must be 1-256 characters containing only: letters, numbers, underscores,
133
+ * hyphens, dots, and colons (URL-safe characters).
134
+ */
135
+ function validateStreamId(streamId) {
136
+ if (!streamId || !streamId.trim()) return {
137
+ valid: false,
138
+ error: `Stream ID cannot be empty`
139
+ };
140
+ const validPattern = /^[a-zA-Z0-9_\-.:]+$/;
141
+ if (!validPattern.test(streamId)) return {
142
+ valid: false,
143
+ error: `Invalid stream ID: "${streamId}"\n Stream IDs can only contain letters, numbers, underscores, hyphens, dots, and colons`
144
+ };
145
+ if (streamId.length > 256) return {
146
+ valid: false,
147
+ error: `Stream ID too long (${streamId.length} chars)\n Maximum length is 256 characters`
148
+ };
149
+ return { valid: true };
150
+ }
151
+
68
152
  //#endregion
69
153
  //#region src/index.ts
70
154
  const STREAM_URL = process.env.STREAM_URL || `http://localhost:4437`;
71
155
  const STREAM_AUTH = process.env.STREAM_AUTH;
72
156
  /**
73
- * Parse global options (like --auth) from args.
74
- * Falls back to STREAM_AUTH env var if --auth flag not provided.
75
- * Returns the parsed options and remaining args.
157
+ * Parse global options (--url, --auth) from args.
158
+ * Falls back to STREAM_URL/STREAM_AUTH env vars when flags not provided.
159
+ * Returns the parsed options, remaining args, and any warnings.
76
160
  */
77
161
  function parseGlobalOptions(args) {
78
162
  const options = {};
79
163
  const remainingArgs = [];
164
+ const warnings = [];
80
165
  for (let i = 0; i < args.length; i++) {
81
166
  const arg = args[i];
82
- if (arg === `--auth`) {
167
+ if (arg === `--url`) {
168
+ const value = args[i + 1];
169
+ if (!value || value.startsWith(`--`)) throw new Error(`--url requires a value\n Example: --url "http://localhost:4437"`);
170
+ const urlValidation = validateUrl(value);
171
+ if (!urlValidation.valid) throw new Error(urlValidation.error);
172
+ options.url = normalizeBaseUrl(value);
173
+ i++;
174
+ } else if (arg === `--auth`) {
83
175
  const value = args[i + 1];
84
- if (!value || value.startsWith(`--`)) throw new Error(`--auth requires a value (e.g., --auth "Bearer token")`);
85
- if (!value.trim()) throw new Error(`--auth value cannot be empty or whitespace`);
176
+ if (!value || value.startsWith(`--`)) throw new Error(`--auth requires a value\n Example: --auth "Bearer my-token"`);
177
+ const authValidation = validateAuth(value);
178
+ if (!authValidation.valid) throw new Error(authValidation.error);
179
+ if (authValidation.warning) warnings.push(authValidation.warning);
86
180
  options.auth = value;
87
181
  i++;
88
182
  } else remainingArgs.push(arg);
89
183
  }
184
+ if (!options.url) {
185
+ const urlValidation = validateUrl(STREAM_URL);
186
+ if (!urlValidation.valid) throw new Error(`Invalid STREAM_URL environment variable: ${urlValidation.error}`);
187
+ options.url = normalizeBaseUrl(STREAM_URL);
188
+ }
90
189
  if (!options.auth && STREAM_AUTH) {
91
- if (!STREAM_AUTH.trim()) throw new Error(`STREAM_AUTH environment variable cannot be empty or whitespace`);
190
+ const authValidation = validateAuth(STREAM_AUTH);
191
+ if (!authValidation.valid) throw new Error(`Invalid STREAM_AUTH environment variable: ${authValidation.error}`);
192
+ if (authValidation.warning) warnings.push(authValidation.warning);
92
193
  options.auth = STREAM_AUTH;
93
194
  }
94
195
  return {
95
196
  options,
96
- remainingArgs
197
+ remainingArgs,
198
+ warnings
97
199
  };
98
200
  }
201
+ function getErrorMessage(error) {
202
+ return error instanceof Error ? error.message : String(error);
203
+ }
99
204
  function buildHeaders(options) {
100
- if (options.auth) return { Authorization: options.auth };
101
- return {};
205
+ return options.auth ? { Authorization: options.auth } : {};
102
206
  }
103
- function printUsage() {
104
- console.error(`
207
+ function getUsageText() {
208
+ return `
105
209
  Usage:
106
210
  durable-stream create <stream_id> Create a new stream
107
211
  durable-stream write <stream_id> <content> Write content to a stream
@@ -110,7 +214,9 @@ Usage:
110
214
  durable-stream delete <stream_id> Delete a stream
111
215
 
112
216
  Global Options:
217
+ --url <url> Stream server URL (overrides STREAM_URL env var)
113
218
  --auth <value> Authorization header value (e.g., "Bearer my-token")
219
+ --help, -h Show this help message
114
220
 
115
221
  Write Options:
116
222
  --content-type <type> Content-Type for the message (default: application/octet-stream)
@@ -120,191 +226,255 @@ Write Options:
120
226
  Environment Variables:
121
227
  STREAM_URL Base URL of the stream server (default: http://localhost:4437)
122
228
  STREAM_AUTH Authorization header value (overridden by --auth flag)
123
- `);
229
+ `;
124
230
  }
125
- async function createStream(streamId, headers) {
126
- const url = `${STREAM_URL}/v1/stream/${streamId}`;
231
+ function printUsage({ to = `stderr` } = {}) {
232
+ const out = to === `stderr` ? stderr : stdout;
233
+ out.write(getUsageText());
234
+ }
235
+ async function createStream(baseUrl, streamId, headers) {
236
+ const url = `${baseUrl}/v1/stream/${streamId}`;
127
237
  try {
128
238
  await DurableStream.create({
129
239
  url,
130
240
  headers,
131
241
  contentType: `application/octet-stream`
132
242
  });
133
- console.log(`Created stream: ${streamId}`);
243
+ console.log(`Stream created successfully: "${streamId}"`);
244
+ console.log(` URL: ${url}`);
134
245
  } catch (error) {
135
- const message = error instanceof Error ? error.message : String(error);
136
- stderr.write(`Error creating stream: ${message}\n`);
246
+ stderr.write(`Failed to create stream "${streamId}"\n`);
247
+ stderr.write(` ${formatErrorMessage(getErrorMessage(error))}\n`);
137
248
  process.exit(1);
138
249
  }
139
250
  }
140
251
  /**
141
- * Append JSON data to a stream with one-level array flattening.
252
+ * Format error messages from the server/client for better readability.
142
253
  */
143
- async function appendJson(stream, parsed) {
144
- let count = 0;
145
- for (const item of flattenJsonForAppend(parsed)) {
146
- await stream.append(item);
147
- count++;
254
+ function formatErrorMessage(message) {
255
+ const httpMatch = message.match(/HTTP Error (\d+)/);
256
+ const statusCode = httpMatch?.[1];
257
+ if (statusCode) {
258
+ const status = parseInt(statusCode, 10);
259
+ const statusText = getHttpStatusText(status);
260
+ return message.replace(/HTTP Error \d+/, `${statusText} (${status})`);
148
261
  }
149
- return count;
262
+ return message;
263
+ }
264
+ function getHttpStatusText(status) {
265
+ return STATUS_CODES[status] ?? `HTTP Error`;
266
+ }
267
+ /**
268
+ * Append JSON data to a stream using batch semantics.
269
+ * Arrays are flattened one level (each element becomes a separate message).
270
+ * Non-array values are written as a single message.
271
+ * Returns the number of messages written.
272
+ */
273
+ async function appendJsonBatch(stream, parsed) {
274
+ const items = [...flattenJsonForAppend(parsed)];
275
+ for (const item of items) await stream.append(JSON.stringify(item));
276
+ return items.length;
277
+ }
278
+ /**
279
+ * Read all data from stdin into a Buffer.
280
+ */
281
+ async function readStdin() {
282
+ const chunks = [];
283
+ stdin.on(`data`, (chunk) => {
284
+ chunks.push(chunk);
285
+ });
286
+ await new Promise((resolve$1, reject) => {
287
+ stdin.on(`end`, resolve$1);
288
+ stdin.on(`error`, (err) => {
289
+ reject(new Error(`Failed to read from stdin: ${err.message}`));
290
+ });
291
+ });
292
+ return Buffer.concat(chunks);
150
293
  }
151
- async function writeStream(streamId, contentType, batchJson, headers, content) {
152
- const url = `${STREAM_URL}/v1/stream/${streamId}`;
294
+ /**
295
+ * Process escape sequences in content string.
296
+ */
297
+ function processEscapeSequences(content) {
298
+ return content.replace(/\\n/g, `\n`).replace(/\\t/g, `\t`).replace(/\\r/g, `\r`).replace(/\\\\/g, `\\`);
299
+ }
300
+ async function writeStream(baseUrl, streamId, contentType, batchJson, headers, content) {
301
+ const url = `${baseUrl}/v1/stream/${streamId}`;
153
302
  const isJson = isJsonContentType(contentType);
303
+ let data;
304
+ let source;
305
+ if (content) {
306
+ data = processEscapeSequences(content);
307
+ source = `argument`;
308
+ } else {
309
+ data = await readStdin();
310
+ source = `stdin`;
311
+ if (data.length === 0) {
312
+ stderr.write(`No data received from stdin\n`);
313
+ process.exit(1);
314
+ }
315
+ }
154
316
  try {
155
317
  const stream = new DurableStream({
156
318
  url,
157
319
  headers,
158
320
  contentType
159
321
  });
160
- if (content) {
161
- const processedContent = content.replace(/\\n/g, `\n`).replace(/\\t/g, `\t`).replace(/\\r/g, `\r`).replace(/\\\\/g, `\\`);
162
- if (isJson) {
163
- const parsed = JSON.parse(processedContent);
164
- if (batchJson) {
165
- const count = await appendJson(stream, parsed);
166
- console.log(`Wrote ${count} message(s) to ${streamId}`);
322
+ if (isJson) {
323
+ const jsonString = typeof data === `string` ? data : data.toString(`utf8`);
324
+ let parsed;
325
+ try {
326
+ parsed = JSON.parse(jsonString);
327
+ } catch (parseError) {
328
+ const parseMessage = parseError instanceof SyntaxError ? parseError.message : `Unknown parsing error`;
329
+ if (source === `argument`) {
330
+ const preview = jsonString.slice(0, 100);
331
+ const ellipsis = jsonString.length > 100 ? `...` : ``;
332
+ stderr.write(`Failed to parse JSON content\n`);
333
+ stderr.write(` ${parseMessage}\n`);
334
+ stderr.write(` Input: ${preview}${ellipsis}\n`);
167
335
  } else {
168
- await stream.append(parsed);
169
- console.log(`Wrote 1 message to ${streamId}`);
336
+ stderr.write(`Failed to parse JSON from stdin\n`);
337
+ stderr.write(` ${parseMessage}\n`);
170
338
  }
171
- } else {
172
- await stream.append(processedContent);
173
- console.log(`Wrote ${processedContent.length} bytes to ${streamId}`);
339
+ process.exit(1);
174
340
  }
175
- } else {
176
- const chunks = [];
177
- stdin.on(`data`, (chunk) => {
178
- chunks.push(chunk);
179
- });
180
- await new Promise((resolve$1, reject) => {
181
- stdin.on(`end`, resolve$1);
182
- stdin.on(`error`, reject);
183
- });
184
- const data = Buffer.concat(chunks);
185
- if (isJson) {
186
- const parsed = JSON.parse(data.toString(`utf8`));
187
- if (batchJson) {
188
- const count = await appendJson(stream, parsed);
189
- console.log(`Wrote ${count} message(s) to ${streamId}`);
190
- } else {
191
- await stream.append(parsed);
192
- console.log(`Wrote 1 message to ${streamId}`);
193
- }
341
+ if (batchJson) {
342
+ const count = await appendJsonBatch(stream, parsed);
343
+ console.log(`Wrote ${count} message${count !== 1 ? `s` : ``} to stream "${streamId}"`);
194
344
  } else {
195
- await stream.append(data);
196
- console.log(`Wrote ${data.length} bytes to ${streamId}`);
345
+ await stream.append(JSON.stringify(parsed));
346
+ console.log(`Wrote 1 JSON message to stream "${streamId}"`);
197
347
  }
348
+ } else {
349
+ await stream.append(data);
350
+ const byteCount = typeof data === `string` ? Buffer.byteLength(data, `utf8`) : data.length;
351
+ console.log(`Wrote ${formatBytes(byteCount)} to stream "${streamId}"`);
198
352
  }
199
353
  } catch (error) {
200
- const message = error instanceof Error ? error.message : String(error);
201
- stderr.write(`Error writing to stream: ${message}\n`);
354
+ stderr.write(`Failed to write to stream "${streamId}"\n`);
355
+ stderr.write(` ${formatErrorMessage(getErrorMessage(error))}\n`);
202
356
  process.exit(1);
203
357
  }
204
358
  }
205
- async function readStream(streamId, headers) {
206
- const url = `${STREAM_URL}/v1/stream/${streamId}`;
359
+ function formatBytes(bytes) {
360
+ if (bytes === 1) return `1 byte`;
361
+ if (bytes < 1024) return `${bytes} bytes`;
362
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
363
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
364
+ }
365
+ async function readStream(baseUrl, streamId, headers) {
366
+ const url = `${baseUrl}/v1/stream/${streamId}`;
207
367
  try {
208
368
  const stream = new DurableStream({
209
369
  url,
210
370
  headers
211
371
  });
212
- const res = await stream.stream({ live: `auto` });
372
+ const res = await stream.stream({ live: true });
213
373
  for await (const chunk of res.bodyStream()) if (chunk.length > 0) stdout.write(chunk);
214
374
  } catch (error) {
215
- const message = error instanceof Error ? error.message : String(error);
216
- stderr.write(`Error reading stream: ${message}\n`);
375
+ stderr.write(`Failed to read stream "${streamId}"\n`);
376
+ stderr.write(` ${formatErrorMessage(getErrorMessage(error))}\n`);
217
377
  process.exit(1);
218
378
  }
219
379
  }
220
- async function deleteStream(streamId, headers) {
221
- const url = `${STREAM_URL}/v1/stream/${streamId}`;
380
+ async function deleteStream(baseUrl, streamId, headers) {
381
+ const url = `${baseUrl}/v1/stream/${streamId}`;
222
382
  try {
223
383
  const stream = new DurableStream({
224
384
  url,
225
385
  headers
226
386
  });
227
387
  await stream.delete();
228
- console.log(`Deleted stream: ${streamId}`);
388
+ console.log(`Stream deleted successfully: "${streamId}"`);
229
389
  } catch (error) {
230
- const message = error instanceof Error ? error.message : String(error);
231
- stderr.write(`Error deleting stream: ${message}\n`);
390
+ stderr.write(`Failed to delete stream "${streamId}"\n`);
391
+ stderr.write(` ${formatErrorMessage(getErrorMessage(error))}\n`);
232
392
  process.exit(1);
233
393
  }
234
394
  }
235
395
  async function main() {
396
+ const rawArgs = process.argv.slice(2);
397
+ if (rawArgs.includes(`--help`) || rawArgs.includes(`-h`)) {
398
+ printUsage({ to: `stdout` });
399
+ process.exit(0);
400
+ }
236
401
  let options;
237
402
  let args;
403
+ let warnings;
238
404
  try {
239
- const parsed = parseGlobalOptions(process.argv.slice(2));
405
+ const parsed = parseGlobalOptions(rawArgs);
240
406
  options = parsed.options;
241
407
  args = parsed.remainingArgs;
408
+ warnings = parsed.warnings;
242
409
  } catch (error) {
243
- const message = error instanceof Error ? error.message : String(error);
244
- stderr.write(`Error: ${message}\n`);
410
+ stderr.write(`Error: ${getErrorMessage(error)}\n`);
245
411
  process.exit(1);
246
412
  }
413
+ for (const warning of warnings) stderr.write(`${warning}\n`);
247
414
  const headers = buildHeaders(options);
248
415
  if (args.length < 1) {
416
+ stderr.write(`Error: No command specified\n`);
249
417
  printUsage();
250
418
  process.exit(1);
251
419
  }
252
420
  const command = args[0];
421
+ function getStreamId() {
422
+ if (args.length < 2) {
423
+ stderr.write(`Error: Missing stream_id\n`);
424
+ stderr.write(` Usage: durable-stream ${command} <stream_id>\n`);
425
+ process.exit(1);
426
+ }
427
+ const streamId = args[1];
428
+ const validation = validateStreamId(streamId);
429
+ if (!validation.valid) {
430
+ stderr.write(`Error: ${validation.error}\n`);
431
+ process.exit(1);
432
+ }
433
+ return streamId;
434
+ }
253
435
  switch (command) {
254
436
  case `create`: {
255
- if (args.length < 2) {
256
- stderr.write(`Error: stream_id required\n`);
257
- printUsage();
258
- process.exit(1);
259
- }
260
- await createStream(args[1], headers);
437
+ const streamId = getStreamId();
438
+ await createStream(options.url, streamId, headers);
261
439
  break;
262
440
  }
263
441
  case `write`: {
264
- if (args.length < 2) {
265
- stderr.write(`Error: stream_id required\n`);
266
- printUsage();
267
- process.exit(1);
268
- }
269
- const streamId = args[1];
442
+ const streamId = getStreamId();
270
443
  let parsed;
271
444
  try {
272
445
  parsed = parseWriteArgs(args.slice(2));
273
446
  } catch (error) {
274
- const message = error instanceof Error ? error.message : String(error);
275
- stderr.write(`Error: ${message}\n`);
447
+ stderr.write(`Error: ${getErrorMessage(error)}\n`);
276
448
  process.exit(1);
277
449
  }
278
- if (!stdin.isTTY) await writeStream(streamId, parsed.contentType, parsed.batchJson, headers);
279
- else if (parsed.content) await writeStream(streamId, parsed.contentType, parsed.batchJson, headers, parsed.content);
450
+ const hasContent = parsed.content || !stdin.isTTY;
451
+ if (hasContent) await writeStream(options.url, streamId, parsed.contentType, parsed.batchJson, headers, parsed.content || void 0);
280
452
  else {
281
- stderr.write(`Error: content required (provide as argument or pipe to stdin)\n`);
282
- printUsage();
453
+ stderr.write(`Error: No content provided\n`);
454
+ stderr.write(` Provide content as an argument or pipe from stdin:\n`);
455
+ stderr.write(` durable-stream write ${streamId} "your content here"\n`);
456
+ stderr.write(` echo "content" | durable-stream write ${streamId}\n`);
283
457
  process.exit(1);
284
458
  }
285
459
  break;
286
460
  }
287
461
  case `read`: {
288
- if (args.length < 2) {
289
- stderr.write(`Error: stream_id required\n`);
290
- printUsage();
291
- process.exit(1);
292
- }
293
- await readStream(args[1], headers);
462
+ const streamId = getStreamId();
463
+ await readStream(options.url, streamId, headers);
294
464
  break;
295
465
  }
296
466
  case `delete`: {
297
- if (args.length < 2) {
298
- stderr.write(`Error: stream_id required\n`);
299
- printUsage();
300
- process.exit(1);
301
- }
302
- await deleteStream(args[1], headers);
467
+ const streamId = getStreamId();
468
+ await deleteStream(options.url, streamId, headers);
303
469
  break;
304
470
  }
305
471
  default:
306
- stderr.write(`Error: unknown command '${command}'\n`);
307
- printUsage();
472
+ if (command?.startsWith(`-`)) stderr.write(`Error: Unknown option "${command}"\n`);
473
+ else {
474
+ stderr.write(`Error: Unknown command "${command}"\n`);
475
+ stderr.write(` Available commands: create, write, read, delete\n`);
476
+ }
477
+ stderr.write(` Run "durable-stream --help" for usage information\n`);
308
478
  process.exit(1);
309
479
  }
310
480
  }
@@ -315,10 +485,9 @@ function isMainModule() {
315
485
  return scriptPath === modulePath;
316
486
  }
317
487
  if (isMainModule()) main().catch((error) => {
318
- const message = error instanceof Error ? error.message : String(error);
319
- stderr.write(`Fatal error: ${message}\n`);
488
+ stderr.write(`Fatal error: ${getErrorMessage(error)}\n`);
320
489
  process.exit(1);
321
490
  });
322
491
 
323
492
  //#endregion
324
- export { buildHeaders, flattenJsonForAppend, isJsonContentType, parseGlobalOptions, parseWriteArgs };
493
+ export { buildHeaders, flattenJsonForAppend, getUsageText, isJsonContentType, normalizeBaseUrl, parseGlobalOptions, parseWriteArgs, validateAuth, validateStreamId, validateUrl };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@durable-streams/cli",
3
3
  "description": "CLI tool for working with Durable Streams",
4
- "version": "0.1.7",
4
+ "version": "0.1.8",
5
5
  "author": "Durable Stream contributors",
6
6
  "bin": {
7
7
  "cli": "./dist/index.js",
@@ -12,7 +12,7 @@
12
12
  "url": "https://github.com/durable-streams/durable-streams/issues"
13
13
  },
14
14
  "dependencies": {
15
- "@durable-streams/client": "0.1.5"
15
+ "@durable-streams/client": "0.2.0"
16
16
  },
17
17
  "devDependencies": {
18
18
  "@types/node": "^22.15.21",
@@ -20,7 +20,7 @@
20
20
  "tsx": "^4.19.2",
21
21
  "typescript": "^5.5.2",
22
22
  "vitest": "^3.1.3",
23
- "@durable-streams/server": "0.1.6"
23
+ "@durable-streams/server": "0.1.7"
24
24
  },
25
25
  "engines": {
26
26
  "node": ">=18.0.0"