@durable-streams/cli 0.1.6 → 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/README.md CHANGED
@@ -69,6 +69,11 @@ durable-stream-dev read my-stream
69
69
  ### Environment Variables
70
70
 
71
71
  - `STREAM_URL` - Base URL of the stream server (default: `http://localhost:4437`)
72
+ - `STREAM_AUTH` - Authorization header value (e.g., `Bearer my-token`)
73
+
74
+ ### Global Options
75
+
76
+ - `--auth <value>` - Authorization header value (overrides `STREAM_AUTH` env var)
72
77
 
73
78
  ### Write Options
74
79
 
@@ -125,6 +130,23 @@ durable-stream-dev read <stream_id>
125
130
  durable-stream-dev delete <stream_id>
126
131
  ```
127
132
 
133
+ ### Authentication
134
+
135
+ Use the `--auth` flag or `STREAM_AUTH` environment variable to authenticate:
136
+
137
+ ```bash
138
+ # Using environment variable
139
+ export STREAM_AUTH="Bearer my-token"
140
+ durable-stream-dev read my-stream
141
+
142
+ # Using --auth flag (overrides env var)
143
+ durable-stream-dev --auth "Bearer my-token" read my-stream
144
+
145
+ # Works with any auth scheme
146
+ durable-stream-dev --auth "Basic dXNlcjpwYXNz" read my-stream
147
+ durable-stream-dev --auth "ApiKey abc123" read my-stream
148
+ ```
149
+
128
150
  ## Complete Example Workflow
129
151
 
130
152
  ```bash
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,11 +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
- function printUsage() {
96
- console.error(`
179
+ const STREAM_AUTH = process.env.STREAM_AUTH;
180
+ /**
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.
184
+ */
185
+ function parseGlobalOptions(args) {
186
+ const options = {};
187
+ const remainingArgs = [];
188
+ const warnings = [];
189
+ for (let i = 0; i < args.length; i++) {
190
+ const arg = args[i];
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`) {
199
+ const value = args[i + 1];
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);
204
+ options.auth = value;
205
+ i++;
206
+ } else remainingArgs.push(arg);
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
+ }
213
+ if (!options.auth && STREAM_AUTH) {
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);
217
+ options.auth = STREAM_AUTH;
218
+ }
219
+ return {
220
+ options,
221
+ remainingArgs,
222
+ warnings
223
+ };
224
+ }
225
+ function getErrorMessage(error) {
226
+ return error instanceof Error ? error.message : String(error);
227
+ }
228
+ function buildHeaders(options) {
229
+ return options.auth ? { Authorization: options.auth } : {};
230
+ }
231
+ function getUsageText() {
232
+ return `
97
233
  Usage:
98
234
  durable-stream create <stream_id> Create a new stream
99
235
  durable-stream write <stream_id> <content> Write content to a stream
@@ -101,6 +237,11 @@ Usage:
101
237
  durable-stream read <stream_id> Follow a stream and write to stdout
102
238
  durable-stream delete <stream_id> Delete a stream
103
239
 
240
+ Global Options:
241
+ --url <url> Stream server URL (overrides STREAM_URL env var)
242
+ --auth <value> Authorization header value (e.g., "Bearer my-token")
243
+ --help, -h Show this help message
244
+
104
245
  Write Options:
105
246
  --content-type <type> Content-Type for the message (default: application/octet-stream)
106
247
  --json Write as JSON (input stored as single message)
@@ -108,167 +249,256 @@ Write Options:
108
249
 
109
250
  Environment Variables:
110
251
  STREAM_URL Base URL of the stream server (default: http://localhost:4437)
111
- `);
252
+ STREAM_AUTH Authorization header value (overridden by --auth flag)
253
+ `;
254
+ }
255
+ function printUsage({ to = `stderr` } = {}) {
256
+ const out = to === `stderr` ? node_process.stderr : node_process.stdout;
257
+ out.write(getUsageText());
112
258
  }
113
- async function createStream(streamId) {
114
- const url = `${STREAM_URL}/v1/stream/${streamId}`;
259
+ async function createStream(baseUrl, streamId, headers) {
260
+ const url = `${baseUrl}/v1/stream/${streamId}`;
115
261
  try {
116
262
  await __durable_streams_client.DurableStream.create({
117
263
  url,
264
+ headers,
118
265
  contentType: `application/octet-stream`
119
266
  });
120
- console.log(`Created stream: ${streamId}`);
267
+ console.log(`Stream created successfully: "${streamId}"`);
268
+ console.log(` URL: ${url}`);
121
269
  } catch (error) {
122
- if (error instanceof Error) node_process.stderr.write(`Error creating stream: ${error.message}\n`);
270
+ node_process.stderr.write(`Failed to create stream "${streamId}"\n`);
271
+ node_process.stderr.write(` ${formatErrorMessage(getErrorMessage(error))}\n`);
123
272
  process.exit(1);
124
273
  }
125
274
  }
126
275
  /**
127
- * Append JSON data to a stream with one-level array flattening.
276
+ * Format error messages from the server/client for better readability.
128
277
  */
129
- async function appendJson(stream, parsed) {
130
- let count = 0;
131
- for (const item of flattenJsonForAppend(parsed)) {
132
- await stream.append(item);
133
- 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})`);
134
285
  }
135
- return count;
286
+ return message;
136
287
  }
137
- async function writeStream(streamId, contentType, batchJson, content) {
138
- const url = `${STREAM_URL}/v1/stream/${streamId}`;
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);
317
+ }
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}`;
139
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
+ }
140
340
  try {
141
341
  const stream = new __durable_streams_client.DurableStream({
142
342
  url,
343
+ headers,
143
344
  contentType
144
345
  });
145
- if (content) {
146
- const processedContent = content.replace(/\\n/g, `\n`).replace(/\\t/g, `\t`).replace(/\\r/g, `\r`).replace(/\\\\/g, `\\`);
147
- if (isJson) {
148
- const parsed = JSON.parse(processedContent);
149
- if (batchJson) {
150
- const count = await appendJson(stream, parsed);
151
- 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`);
152
359
  } else {
153
- await stream.append(parsed);
154
- 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`);
155
362
  }
156
- } else {
157
- await stream.append(processedContent);
158
- console.log(`Wrote ${processedContent.length} bytes to ${streamId}`);
363
+ process.exit(1);
159
364
  }
160
- } else {
161
- const chunks = [];
162
- node_process.stdin.on(`data`, (chunk) => {
163
- chunks.push(chunk);
164
- });
165
- await new Promise((resolve, reject) => {
166
- node_process.stdin.on(`end`, resolve);
167
- node_process.stdin.on(`error`, reject);
168
- });
169
- const data = Buffer.concat(chunks);
170
- if (isJson) {
171
- const parsed = JSON.parse(data.toString(`utf8`));
172
- if (batchJson) {
173
- const count = await appendJson(stream, parsed);
174
- console.log(`Wrote ${count} message(s) to ${streamId}`);
175
- } else {
176
- await stream.append(parsed);
177
- console.log(`Wrote 1 message to ${streamId}`);
178
- }
365
+ if (batchJson) {
366
+ const count = await appendJsonBatch(stream, parsed);
367
+ console.log(`Wrote ${count} message${count !== 1 ? `s` : ``} to stream "${streamId}"`);
179
368
  } else {
180
- await stream.append(data);
181
- 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}"`);
182
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}"`);
183
376
  }
184
377
  } catch (error) {
185
- if (error instanceof Error) node_process.stderr.write(`Error writing to stream: ${error.message}\n`);
378
+ node_process.stderr.write(`Failed to write to stream "${streamId}"\n`);
379
+ node_process.stderr.write(` ${formatErrorMessage(getErrorMessage(error))}\n`);
186
380
  process.exit(1);
187
381
  }
188
382
  }
189
- async function readStream(streamId) {
190
- 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}`;
191
391
  try {
192
- const stream = new __durable_streams_client.DurableStream({ url });
193
- const res = await stream.stream({ live: `auto` });
392
+ const stream = new __durable_streams_client.DurableStream({
393
+ url,
394
+ headers
395
+ });
396
+ const res = await stream.stream({ live: true });
194
397
  for await (const chunk of res.bodyStream()) if (chunk.length > 0) node_process.stdout.write(chunk);
195
398
  } catch (error) {
196
- if (error instanceof Error) node_process.stderr.write(`Error reading stream: ${error.message}\n`);
399
+ node_process.stderr.write(`Failed to read stream "${streamId}"\n`);
400
+ node_process.stderr.write(` ${formatErrorMessage(getErrorMessage(error))}\n`);
197
401
  process.exit(1);
198
402
  }
199
403
  }
200
- async function deleteStream(streamId) {
201
- const url = `${STREAM_URL}/v1/stream/${streamId}`;
404
+ async function deleteStream(baseUrl, streamId, headers) {
405
+ const url = `${baseUrl}/v1/stream/${streamId}`;
202
406
  try {
203
- const stream = new __durable_streams_client.DurableStream({ url });
407
+ const stream = new __durable_streams_client.DurableStream({
408
+ url,
409
+ headers
410
+ });
204
411
  await stream.delete();
205
- console.log(`Deleted stream: ${streamId}`);
412
+ console.log(`Stream deleted successfully: "${streamId}"`);
206
413
  } catch (error) {
207
- if (error instanceof Error) node_process.stderr.write(`Error deleting stream: ${error.message}\n`);
414
+ node_process.stderr.write(`Failed to delete stream "${streamId}"\n`);
415
+ node_process.stderr.write(` ${formatErrorMessage(getErrorMessage(error))}\n`);
208
416
  process.exit(1);
209
417
  }
210
418
  }
211
419
  async function main() {
212
- const args = process.argv.slice(2);
420
+ const rawArgs = process.argv.slice(2);
421
+ if (rawArgs.includes(`--help`) || rawArgs.includes(`-h`)) {
422
+ printUsage({ to: `stdout` });
423
+ process.exit(0);
424
+ }
425
+ let options;
426
+ let args;
427
+ let warnings;
428
+ try {
429
+ const parsed = parseGlobalOptions(rawArgs);
430
+ options = parsed.options;
431
+ args = parsed.remainingArgs;
432
+ warnings = parsed.warnings;
433
+ } catch (error) {
434
+ node_process.stderr.write(`Error: ${getErrorMessage(error)}\n`);
435
+ process.exit(1);
436
+ }
437
+ for (const warning of warnings) node_process.stderr.write(`${warning}\n`);
438
+ const headers = buildHeaders(options);
213
439
  if (args.length < 1) {
440
+ node_process.stderr.write(`Error: No command specified\n`);
214
441
  printUsage();
215
442
  process.exit(1);
216
443
  }
217
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
+ }
218
459
  switch (command) {
219
460
  case `create`: {
220
- if (args.length < 2) {
221
- node_process.stderr.write(`Error: stream_id required\n`);
222
- printUsage();
223
- process.exit(1);
224
- }
225
- await createStream(args[1]);
461
+ const streamId = getStreamId();
462
+ await createStream(options.url, streamId, headers);
226
463
  break;
227
464
  }
228
465
  case `write`: {
229
- if (args.length < 2) {
230
- node_process.stderr.write(`Error: stream_id required\n`);
231
- printUsage();
232
- process.exit(1);
233
- }
234
- const streamId = args[1];
466
+ const streamId = getStreamId();
235
467
  let parsed;
236
468
  try {
237
469
  parsed = parseWriteArgs(args.slice(2));
238
470
  } catch (error) {
239
- if (error instanceof Error) node_process.stderr.write(`Error: ${error.message}\n`);
471
+ node_process.stderr.write(`Error: ${getErrorMessage(error)}\n`);
240
472
  process.exit(1);
241
473
  }
242
- if (!node_process.stdin.isTTY) await writeStream(streamId, parsed.contentType, parsed.batchJson);
243
- else if (parsed.content) await writeStream(streamId, parsed.contentType, parsed.batchJson, 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);
244
476
  else {
245
- node_process.stderr.write(`Error: content required (provide as argument or pipe to stdin)\n`);
246
- 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`);
247
481
  process.exit(1);
248
482
  }
249
483
  break;
250
484
  }
251
485
  case `read`: {
252
- if (args.length < 2) {
253
- node_process.stderr.write(`Error: stream_id required\n`);
254
- printUsage();
255
- process.exit(1);
256
- }
257
- await readStream(args[1]);
486
+ const streamId = getStreamId();
487
+ await readStream(options.url, streamId, headers);
258
488
  break;
259
489
  }
260
490
  case `delete`: {
261
- if (args.length < 2) {
262
- node_process.stderr.write(`Error: stream_id required\n`);
263
- printUsage();
264
- process.exit(1);
265
- }
266
- await deleteStream(args[1]);
491
+ const streamId = getStreamId();
492
+ await deleteStream(options.url, streamId, headers);
267
493
  break;
268
494
  }
269
495
  default:
270
- node_process.stderr.write(`Error: unknown command '${command}'\n`);
271
- 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`);
272
502
  process.exit(1);
273
503
  }
274
504
  }
@@ -279,11 +509,18 @@ function isMainModule() {
279
509
  return scriptPath === modulePath;
280
510
  }
281
511
  if (isMainModule()) main().catch((error) => {
282
- node_process.stderr.write(`Fatal error: ${error.message}\n`);
512
+ node_process.stderr.write(`Fatal error: ${getErrorMessage(error)}\n`);
283
513
  process.exit(1);
284
514
  });
285
515
 
286
516
  //#endregion
517
+ exports.buildHeaders = buildHeaders
287
518
  exports.flattenJsonForAppend = flattenJsonForAppend
519
+ exports.getUsageText = getUsageText
288
520
  exports.isJsonContentType = isJsonContentType
289
- exports.parseWriteArgs = parseWriteArgs
521
+ exports.normalizeBaseUrl = normalizeBaseUrl
522
+ exports.parseGlobalOptions = parseGlobalOptions
523
+ exports.parseWriteArgs = parseWriteArgs
524
+ exports.validateAuth = validateAuth
525
+ exports.validateStreamId = validateStreamId
526
+ exports.validateUrl = validateUrl
package/dist/index.d.cts CHANGED
@@ -30,4 +30,56 @@ interface ParsedWriteArgs {
30
30
  declare function parseWriteArgs(args: Array<string>): ParsedWriteArgs;
31
31
 
32
32
  //#endregion
33
- export { ParsedWriteArgs, flattenJsonForAppend, isJsonContentType, parseWriteArgs };
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
+
65
+ //#endregion
66
+ //#region src/index.d.ts
67
+ interface GlobalOptions {
68
+ url?: string;
69
+ auth?: string;
70
+ }
71
+ /**
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.
75
+ */
76
+ declare function parseGlobalOptions(args: Array<string>): {
77
+ options: GlobalOptions;
78
+ remainingArgs: Array<string>;
79
+ warnings: Array<string>;
80
+ };
81
+ declare function buildHeaders(options: GlobalOptions): Record<string, string>;
82
+ declare function getUsageText(): string;
83
+
84
+ //#endregion
85
+ export { GlobalOptions, ParsedWriteArgs, buildHeaders, flattenJsonForAppend, getUsageText, isJsonContentType, normalizeBaseUrl, parseGlobalOptions, parseWriteArgs, validateAuth, validateStreamId, validateUrl };
package/dist/index.d.ts CHANGED
@@ -30,4 +30,56 @@ interface ParsedWriteArgs {
30
30
  declare function parseWriteArgs(args: Array<string>): ParsedWriteArgs;
31
31
 
32
32
  //#endregion
33
- export { ParsedWriteArgs, flattenJsonForAppend, isJsonContentType, parseWriteArgs };
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
+
65
+ //#endregion
66
+ //#region src/index.d.ts
67
+ interface GlobalOptions {
68
+ url?: string;
69
+ auth?: string;
70
+ }
71
+ /**
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.
75
+ */
76
+ declare function parseGlobalOptions(args: Array<string>): {
77
+ options: GlobalOptions;
78
+ remainingArgs: Array<string>;
79
+ warnings: Array<string>;
80
+ };
81
+ declare function buildHeaders(options: GlobalOptions): Record<string, string>;
82
+ declare function getUsageText(): string;
83
+
84
+ //#endregion
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,11 +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
- function printUsage() {
72
- console.error(`
155
+ const STREAM_AUTH = process.env.STREAM_AUTH;
156
+ /**
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.
160
+ */
161
+ function parseGlobalOptions(args) {
162
+ const options = {};
163
+ const remainingArgs = [];
164
+ const warnings = [];
165
+ for (let i = 0; i < args.length; i++) {
166
+ const arg = args[i];
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`) {
175
+ const value = args[i + 1];
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);
180
+ options.auth = value;
181
+ i++;
182
+ } else remainingArgs.push(arg);
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
+ }
189
+ if (!options.auth && STREAM_AUTH) {
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);
193
+ options.auth = STREAM_AUTH;
194
+ }
195
+ return {
196
+ options,
197
+ remainingArgs,
198
+ warnings
199
+ };
200
+ }
201
+ function getErrorMessage(error) {
202
+ return error instanceof Error ? error.message : String(error);
203
+ }
204
+ function buildHeaders(options) {
205
+ return options.auth ? { Authorization: options.auth } : {};
206
+ }
207
+ function getUsageText() {
208
+ return `
73
209
  Usage:
74
210
  durable-stream create <stream_id> Create a new stream
75
211
  durable-stream write <stream_id> <content> Write content to a stream
@@ -77,6 +213,11 @@ Usage:
77
213
  durable-stream read <stream_id> Follow a stream and write to stdout
78
214
  durable-stream delete <stream_id> Delete a stream
79
215
 
216
+ Global Options:
217
+ --url <url> Stream server URL (overrides STREAM_URL env var)
218
+ --auth <value> Authorization header value (e.g., "Bearer my-token")
219
+ --help, -h Show this help message
220
+
80
221
  Write Options:
81
222
  --content-type <type> Content-Type for the message (default: application/octet-stream)
82
223
  --json Write as JSON (input stored as single message)
@@ -84,167 +225,256 @@ Write Options:
84
225
 
85
226
  Environment Variables:
86
227
  STREAM_URL Base URL of the stream server (default: http://localhost:4437)
87
- `);
228
+ STREAM_AUTH Authorization header value (overridden by --auth flag)
229
+ `;
230
+ }
231
+ function printUsage({ to = `stderr` } = {}) {
232
+ const out = to === `stderr` ? stderr : stdout;
233
+ out.write(getUsageText());
88
234
  }
89
- async function createStream(streamId) {
90
- const url = `${STREAM_URL}/v1/stream/${streamId}`;
235
+ async function createStream(baseUrl, streamId, headers) {
236
+ const url = `${baseUrl}/v1/stream/${streamId}`;
91
237
  try {
92
238
  await DurableStream.create({
93
239
  url,
240
+ headers,
94
241
  contentType: `application/octet-stream`
95
242
  });
96
- console.log(`Created stream: ${streamId}`);
243
+ console.log(`Stream created successfully: "${streamId}"`);
244
+ console.log(` URL: ${url}`);
97
245
  } catch (error) {
98
- if (error instanceof Error) stderr.write(`Error creating stream: ${error.message}\n`);
246
+ stderr.write(`Failed to create stream "${streamId}"\n`);
247
+ stderr.write(` ${formatErrorMessage(getErrorMessage(error))}\n`);
99
248
  process.exit(1);
100
249
  }
101
250
  }
102
251
  /**
103
- * Append JSON data to a stream with one-level array flattening.
252
+ * Format error messages from the server/client for better readability.
104
253
  */
105
- async function appendJson(stream, parsed) {
106
- let count = 0;
107
- for (const item of flattenJsonForAppend(parsed)) {
108
- await stream.append(item);
109
- 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})`);
110
261
  }
111
- return count;
262
+ return message;
112
263
  }
113
- async function writeStream(streamId, contentType, batchJson, content) {
114
- const url = `${STREAM_URL}/v1/stream/${streamId}`;
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);
293
+ }
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}`;
115
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
+ }
116
316
  try {
117
317
  const stream = new DurableStream({
118
318
  url,
319
+ headers,
119
320
  contentType
120
321
  });
121
- if (content) {
122
- const processedContent = content.replace(/\\n/g, `\n`).replace(/\\t/g, `\t`).replace(/\\r/g, `\r`).replace(/\\\\/g, `\\`);
123
- if (isJson) {
124
- const parsed = JSON.parse(processedContent);
125
- if (batchJson) {
126
- const count = await appendJson(stream, parsed);
127
- 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`);
128
335
  } else {
129
- await stream.append(parsed);
130
- console.log(`Wrote 1 message to ${streamId}`);
336
+ stderr.write(`Failed to parse JSON from stdin\n`);
337
+ stderr.write(` ${parseMessage}\n`);
131
338
  }
132
- } else {
133
- await stream.append(processedContent);
134
- console.log(`Wrote ${processedContent.length} bytes to ${streamId}`);
339
+ process.exit(1);
135
340
  }
136
- } else {
137
- const chunks = [];
138
- stdin.on(`data`, (chunk) => {
139
- chunks.push(chunk);
140
- });
141
- await new Promise((resolve$1, reject) => {
142
- stdin.on(`end`, resolve$1);
143
- stdin.on(`error`, reject);
144
- });
145
- const data = Buffer.concat(chunks);
146
- if (isJson) {
147
- const parsed = JSON.parse(data.toString(`utf8`));
148
- if (batchJson) {
149
- const count = await appendJson(stream, parsed);
150
- console.log(`Wrote ${count} message(s) to ${streamId}`);
151
- } else {
152
- await stream.append(parsed);
153
- console.log(`Wrote 1 message to ${streamId}`);
154
- }
341
+ if (batchJson) {
342
+ const count = await appendJsonBatch(stream, parsed);
343
+ console.log(`Wrote ${count} message${count !== 1 ? `s` : ``} to stream "${streamId}"`);
155
344
  } else {
156
- await stream.append(data);
157
- 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}"`);
158
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}"`);
159
352
  }
160
353
  } catch (error) {
161
- if (error instanceof Error) stderr.write(`Error writing to stream: ${error.message}\n`);
354
+ stderr.write(`Failed to write to stream "${streamId}"\n`);
355
+ stderr.write(` ${formatErrorMessage(getErrorMessage(error))}\n`);
162
356
  process.exit(1);
163
357
  }
164
358
  }
165
- async function readStream(streamId) {
166
- 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}`;
167
367
  try {
168
- const stream = new DurableStream({ url });
169
- const res = await stream.stream({ live: `auto` });
368
+ const stream = new DurableStream({
369
+ url,
370
+ headers
371
+ });
372
+ const res = await stream.stream({ live: true });
170
373
  for await (const chunk of res.bodyStream()) if (chunk.length > 0) stdout.write(chunk);
171
374
  } catch (error) {
172
- if (error instanceof Error) stderr.write(`Error reading stream: ${error.message}\n`);
375
+ stderr.write(`Failed to read stream "${streamId}"\n`);
376
+ stderr.write(` ${formatErrorMessage(getErrorMessage(error))}\n`);
173
377
  process.exit(1);
174
378
  }
175
379
  }
176
- async function deleteStream(streamId) {
177
- const url = `${STREAM_URL}/v1/stream/${streamId}`;
380
+ async function deleteStream(baseUrl, streamId, headers) {
381
+ const url = `${baseUrl}/v1/stream/${streamId}`;
178
382
  try {
179
- const stream = new DurableStream({ url });
383
+ const stream = new DurableStream({
384
+ url,
385
+ headers
386
+ });
180
387
  await stream.delete();
181
- console.log(`Deleted stream: ${streamId}`);
388
+ console.log(`Stream deleted successfully: "${streamId}"`);
182
389
  } catch (error) {
183
- if (error instanceof Error) stderr.write(`Error deleting stream: ${error.message}\n`);
390
+ stderr.write(`Failed to delete stream "${streamId}"\n`);
391
+ stderr.write(` ${formatErrorMessage(getErrorMessage(error))}\n`);
184
392
  process.exit(1);
185
393
  }
186
394
  }
187
395
  async function main() {
188
- const args = process.argv.slice(2);
396
+ const rawArgs = process.argv.slice(2);
397
+ if (rawArgs.includes(`--help`) || rawArgs.includes(`-h`)) {
398
+ printUsage({ to: `stdout` });
399
+ process.exit(0);
400
+ }
401
+ let options;
402
+ let args;
403
+ let warnings;
404
+ try {
405
+ const parsed = parseGlobalOptions(rawArgs);
406
+ options = parsed.options;
407
+ args = parsed.remainingArgs;
408
+ warnings = parsed.warnings;
409
+ } catch (error) {
410
+ stderr.write(`Error: ${getErrorMessage(error)}\n`);
411
+ process.exit(1);
412
+ }
413
+ for (const warning of warnings) stderr.write(`${warning}\n`);
414
+ const headers = buildHeaders(options);
189
415
  if (args.length < 1) {
416
+ stderr.write(`Error: No command specified\n`);
190
417
  printUsage();
191
418
  process.exit(1);
192
419
  }
193
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
+ }
194
435
  switch (command) {
195
436
  case `create`: {
196
- if (args.length < 2) {
197
- stderr.write(`Error: stream_id required\n`);
198
- printUsage();
199
- process.exit(1);
200
- }
201
- await createStream(args[1]);
437
+ const streamId = getStreamId();
438
+ await createStream(options.url, streamId, headers);
202
439
  break;
203
440
  }
204
441
  case `write`: {
205
- if (args.length < 2) {
206
- stderr.write(`Error: stream_id required\n`);
207
- printUsage();
208
- process.exit(1);
209
- }
210
- const streamId = args[1];
442
+ const streamId = getStreamId();
211
443
  let parsed;
212
444
  try {
213
445
  parsed = parseWriteArgs(args.slice(2));
214
446
  } catch (error) {
215
- if (error instanceof Error) stderr.write(`Error: ${error.message}\n`);
447
+ stderr.write(`Error: ${getErrorMessage(error)}\n`);
216
448
  process.exit(1);
217
449
  }
218
- if (!stdin.isTTY) await writeStream(streamId, parsed.contentType, parsed.batchJson);
219
- else if (parsed.content) await writeStream(streamId, parsed.contentType, parsed.batchJson, 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);
220
452
  else {
221
- stderr.write(`Error: content required (provide as argument or pipe to stdin)\n`);
222
- 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`);
223
457
  process.exit(1);
224
458
  }
225
459
  break;
226
460
  }
227
461
  case `read`: {
228
- if (args.length < 2) {
229
- stderr.write(`Error: stream_id required\n`);
230
- printUsage();
231
- process.exit(1);
232
- }
233
- await readStream(args[1]);
462
+ const streamId = getStreamId();
463
+ await readStream(options.url, streamId, headers);
234
464
  break;
235
465
  }
236
466
  case `delete`: {
237
- if (args.length < 2) {
238
- stderr.write(`Error: stream_id required\n`);
239
- printUsage();
240
- process.exit(1);
241
- }
242
- await deleteStream(args[1]);
467
+ const streamId = getStreamId();
468
+ await deleteStream(options.url, streamId, headers);
243
469
  break;
244
470
  }
245
471
  default:
246
- stderr.write(`Error: unknown command '${command}'\n`);
247
- 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`);
248
478
  process.exit(1);
249
479
  }
250
480
  }
@@ -255,9 +485,9 @@ function isMainModule() {
255
485
  return scriptPath === modulePath;
256
486
  }
257
487
  if (isMainModule()) main().catch((error) => {
258
- stderr.write(`Fatal error: ${error.message}\n`);
488
+ stderr.write(`Fatal error: ${getErrorMessage(error)}\n`);
259
489
  process.exit(1);
260
490
  });
261
491
 
262
492
  //#endregion
263
- export { flattenJsonForAppend, isJsonContentType, 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.6",
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"