@durable-streams/cli 0.1.8 → 0.2.0

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_fs = __toESM(require("node:fs"));
26
27
  const node_http = __toESM(require("node:http"));
27
28
  const node_path = __toESM(require("node:path"));
28
29
  const node_process = __toESM(require("node:process"));
@@ -53,6 +54,34 @@ function* flattenJsonForAppend(parsed) {
53
54
  //#endregion
54
55
  //#region src/parseWriteArgs.ts
55
56
  /**
57
+ * Extract a flag value from args, supporting both --flag=value and --flag value syntax.
58
+ * Returns { value, consumed } where consumed is the number of args used (0 if no match).
59
+ */
60
+ function extractFlagValue$1(args, index, flagName) {
61
+ const arg = args[index];
62
+ const prefix = `${flagName}=`;
63
+ if (arg.startsWith(prefix)) {
64
+ const value = arg.slice(prefix.length);
65
+ if (!value) throw new Error(`${flagName} requires a value`);
66
+ return {
67
+ value,
68
+ consumed: 1
69
+ };
70
+ }
71
+ if (arg === flagName) {
72
+ const value = args[index + 1];
73
+ if (!value || value.startsWith(`--`)) throw new Error(`${flagName} requires a value`);
74
+ return {
75
+ value,
76
+ consumed: 2
77
+ };
78
+ }
79
+ return {
80
+ value: null,
81
+ consumed: 0
82
+ };
83
+ }
84
+ /**
56
85
  * Parse write command arguments, extracting content-type flags and content.
57
86
  * @param args - Arguments after the stream_id (starting from index 2)
58
87
  * @returns Parsed content type and content string
@@ -73,11 +102,10 @@ function parseWriteArgs(args) {
73
102
  contentType = `application/json`;
74
103
  continue;
75
104
  }
76
- if (arg === `--content-type`) {
77
- const nextArg = args[i + 1];
78
- if (!nextArg || nextArg.startsWith(`--`)) throw new Error(`--content-type requires a value`);
79
- contentType = nextArg;
80
- i++;
105
+ const contentTypeResult = extractFlagValue$1(args, i, `--content-type`);
106
+ if (contentTypeResult.value !== null) {
107
+ contentType = contentTypeResult.value;
108
+ i += contentTypeResult.consumed - 1;
81
109
  continue;
82
110
  }
83
111
  if (arg.startsWith(`--`)) throw new Error(`unknown flag: ${arg}`);
@@ -128,6 +156,20 @@ function normalizeBaseUrl(url) {
128
156
  return url.replace(/\/+$/, ``);
129
157
  }
130
158
  /**
159
+ * Build the full stream URL from a base URL and stream ID.
160
+ * Simply concatenates the stream ID to the base URL.
161
+ *
162
+ * Examples:
163
+ * buildStreamUrl("http://localhost:4437/v1/stream", "my-stream")
164
+ * => "http://localhost:4437/v1/stream/my-stream"
165
+ *
166
+ * buildStreamUrl("http://localhost:3002/v1/stream/my-group", "my-stream")
167
+ * => "http://localhost:3002/v1/stream/my-group/my-stream"
168
+ */
169
+ function buildStreamUrl(baseUrl, streamId) {
170
+ return `${baseUrl}/${streamId}`;
171
+ }
172
+ /**
131
173
  * Validate an authorization header value.
132
174
  * Returns valid=true for any non-empty value, but includes a warning
133
175
  * if the value doesn't match common auth schemes (Bearer, Basic, ApiKey, Token).
@@ -175,9 +217,37 @@ function validateStreamId(streamId) {
175
217
 
176
218
  //#endregion
177
219
  //#region src/index.ts
178
- const STREAM_URL = process.env.STREAM_URL || `http://localhost:4437`;
220
+ const STREAM_URL = process.env.STREAM_URL || `http://localhost:4437/v1/stream`;
179
221
  const STREAM_AUTH = process.env.STREAM_AUTH;
180
222
  /**
223
+ * Extract a flag value from args, supporting both --flag=value and --flag value syntax.
224
+ * Returns { value, consumed } where consumed is the number of args used (0 if no match).
225
+ */
226
+ function extractFlagValue(args, index, flagName, example) {
227
+ const arg = args[index];
228
+ const prefix = `${flagName}=`;
229
+ if (arg.startsWith(prefix)) {
230
+ const value = arg.slice(prefix.length);
231
+ if (!value) throw new Error(`${flagName} requires a value\n Example: ${flagName}="${example}"`);
232
+ return {
233
+ value,
234
+ consumed: 1
235
+ };
236
+ }
237
+ if (arg === flagName) {
238
+ const value = args[index + 1];
239
+ if (!value || value.startsWith(`--`)) throw new Error(`${flagName} requires a value\n Example: ${flagName} "${example}"`);
240
+ return {
241
+ value,
242
+ consumed: 2
243
+ };
244
+ }
245
+ return {
246
+ value: null,
247
+ consumed: 0
248
+ };
249
+ }
250
+ /**
181
251
  * Parse global options (--url, --auth) from args.
182
252
  * Falls back to STREAM_URL/STREAM_AUTH env vars when flags not provided.
183
253
  * Returns the parsed options, remaining args, and any warnings.
@@ -188,22 +258,24 @@ function parseGlobalOptions(args) {
188
258
  const warnings = [];
189
259
  for (let i = 0; i < args.length; i++) {
190
260
  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);
261
+ const urlResult = extractFlagValue(args, i, `--url`, `http://localhost:4437`);
262
+ if (urlResult.value !== null) {
263
+ const urlValidation = validateUrl(urlResult.value);
195
264
  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);
265
+ options.url = normalizeBaseUrl(urlResult.value);
266
+ i += urlResult.consumed - 1;
267
+ continue;
268
+ }
269
+ const authResult = extractFlagValue(args, i, `--auth`, `Bearer my-token`);
270
+ if (authResult.value !== null) {
271
+ const authValidation = validateAuth(authResult.value);
202
272
  if (!authValidation.valid) throw new Error(authValidation.error);
203
273
  if (authValidation.warning) warnings.push(authValidation.warning);
204
- options.auth = value;
205
- i++;
206
- } else remainingArgs.push(arg);
274
+ options.auth = authResult.value;
275
+ i += authResult.consumed - 1;
276
+ continue;
277
+ }
278
+ remainingArgs.push(arg);
207
279
  }
208
280
  if (!options.url) {
209
281
  const urlValidation = validateUrl(STREAM_URL);
@@ -242,13 +314,15 @@ Global Options:
242
314
  --auth <value> Authorization header value (e.g., "Bearer my-token")
243
315
  --help, -h Show this help message
244
316
 
245
- Write Options:
246
- --content-type <type> Content-Type for the message (default: application/octet-stream)
247
- --json Write as JSON (input stored as single message)
317
+ Create/Write Options:
318
+ --content-type <type> Content-Type for the stream (default: application/octet-stream)
319
+ --json Shorthand for --content-type application/json
320
+
321
+ Write-only Options:
248
322
  --batch-json Write as JSON array of messages (each array element stored separately)
249
323
 
250
324
  Environment Variables:
251
- STREAM_URL Base URL of the stream server (default: http://localhost:4437)
325
+ STREAM_URL Base URL of the stream server (default: http://localhost:4437/v1/stream)
252
326
  STREAM_AUTH Authorization header value (overridden by --auth flag)
253
327
  `;
254
328
  }
@@ -256,13 +330,38 @@ function printUsage({ to = `stderr` } = {}) {
256
330
  const out = to === `stderr` ? node_process.stderr : node_process.stdout;
257
331
  out.write(getUsageText());
258
332
  }
259
- async function createStream(baseUrl, streamId, headers) {
260
- const url = `${baseUrl}/v1/stream/${streamId}`;
333
+ /**
334
+ * Parse create command arguments, extracting content-type flags.
335
+ * @param args - Arguments after the stream_id
336
+ * @returns Parsed content type
337
+ * @throws Error if --content-type is missing its value or if unknown flags are provided
338
+ */
339
+ function parseCreateArgs(args) {
340
+ let contentType = `application/octet-stream`;
341
+ for (let i = 0; i < args.length; i++) {
342
+ const arg = args[i];
343
+ if (arg === `--json`) {
344
+ contentType = `application/json`;
345
+ continue;
346
+ }
347
+ const contentTypeResult = extractFlagValue(args, i, `--content-type`, `application/json`);
348
+ if (contentTypeResult.value !== null) {
349
+ contentType = contentTypeResult.value;
350
+ i += contentTypeResult.consumed - 1;
351
+ continue;
352
+ }
353
+ if (arg.startsWith(`--`)) throw new Error(`unknown flag: ${arg}`);
354
+ throw new Error(`unexpected argument: ${arg}`);
355
+ }
356
+ return { contentType };
357
+ }
358
+ async function createStream(baseUrl, streamId, headers, contentType) {
359
+ const url = buildStreamUrl(baseUrl, streamId);
261
360
  try {
262
361
  await __durable_streams_client.DurableStream.create({
263
362
  url,
264
363
  headers,
265
- contentType: `application/octet-stream`
364
+ contentType
266
365
  });
267
366
  console.log(`Stream created successfully: "${streamId}"`);
268
367
  console.log(` URL: ${url}`);
@@ -322,7 +421,7 @@ function processEscapeSequences(content) {
322
421
  return content.replace(/\\n/g, `\n`).replace(/\\t/g, `\t`).replace(/\\r/g, `\r`).replace(/\\\\/g, `\\`);
323
422
  }
324
423
  async function writeStream(baseUrl, streamId, contentType, batchJson, headers, content) {
325
- const url = `${baseUrl}/v1/stream/${streamId}`;
424
+ const url = buildStreamUrl(baseUrl, streamId);
326
425
  const isJson = isJsonContentType(contentType);
327
426
  let data;
328
427
  let source;
@@ -387,7 +486,7 @@ function formatBytes(bytes) {
387
486
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
388
487
  }
389
488
  async function readStream(baseUrl, streamId, headers) {
390
- const url = `${baseUrl}/v1/stream/${streamId}`;
489
+ const url = buildStreamUrl(baseUrl, streamId);
391
490
  try {
392
491
  const stream = new __durable_streams_client.DurableStream({
393
492
  url,
@@ -402,7 +501,7 @@ async function readStream(baseUrl, streamId, headers) {
402
501
  }
403
502
  }
404
503
  async function deleteStream(baseUrl, streamId, headers) {
405
- const url = `${baseUrl}/v1/stream/${streamId}`;
504
+ const url = buildStreamUrl(baseUrl, streamId);
406
505
  try {
407
506
  const stream = new __durable_streams_client.DurableStream({
408
507
  url,
@@ -459,7 +558,14 @@ async function main() {
459
558
  switch (command) {
460
559
  case `create`: {
461
560
  const streamId = getStreamId();
462
- await createStream(options.url, streamId, headers);
561
+ let createArgs;
562
+ try {
563
+ createArgs = parseCreateArgs(args.slice(2));
564
+ } catch (error) {
565
+ node_process.stderr.write(`Error: ${getErrorMessage(error)}\n`);
566
+ process.exit(1);
567
+ }
568
+ await createStream(options.url, streamId, headers, createArgs.contentType);
463
569
  break;
464
570
  }
465
571
  case `write`: {
@@ -492,21 +598,37 @@ async function main() {
492
598
  await deleteStream(options.url, streamId, headers);
493
599
  break;
494
600
  }
495
- default:
496
- if (command?.startsWith(`-`)) node_process.stderr.write(`Error: Unknown option "${command}"\n`);
601
+ default: {
602
+ const commandSpecificFlags = [
603
+ `--content-type`,
604
+ `--json`,
605
+ `--batch-json`
606
+ ];
607
+ if (command && commandSpecificFlags.includes(command)) {
608
+ node_process.stderr.write(`Error: "${command}" must come after the command and stream_id\n`);
609
+ node_process.stderr.write(` Example: durable-stream create <stream_id> ${command} ...\n`);
610
+ node_process.stderr.write(` Example: durable-stream write <stream_id> ${command} ...\n`);
611
+ } else if (command?.startsWith(`-`)) node_process.stderr.write(`Error: Unknown option "${command}"\n`);
497
612
  else {
498
613
  node_process.stderr.write(`Error: Unknown command "${command}"\n`);
499
614
  node_process.stderr.write(` Available commands: create, write, read, delete\n`);
500
615
  }
501
616
  node_process.stderr.write(` Run "durable-stream --help" for usage information\n`);
502
617
  process.exit(1);
618
+ }
503
619
  }
504
620
  }
505
621
  function isMainModule() {
506
622
  if (!process.argv[1]) return false;
507
- const scriptPath = (0, node_path.resolve)(process.argv[1]);
508
- const modulePath = (0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
509
- return scriptPath === modulePath;
623
+ try {
624
+ const scriptPath = (0, node_fs.realpathSync)((0, node_path.resolve)(process.argv[1]));
625
+ const modulePath = (0, node_fs.realpathSync)((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
626
+ return scriptPath === modulePath;
627
+ } catch {
628
+ const scriptPath = (0, node_path.resolve)(process.argv[1]);
629
+ const modulePath = (0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
630
+ return scriptPath === modulePath;
631
+ }
510
632
  }
511
633
  if (isMainModule()) main().catch((error) => {
512
634
  node_process.stderr.write(`Fatal error: ${getErrorMessage(error)}\n`);
@@ -515,6 +637,7 @@ if (isMainModule()) main().catch((error) => {
515
637
 
516
638
  //#endregion
517
639
  exports.buildHeaders = buildHeaders
640
+ exports.buildStreamUrl = buildStreamUrl
518
641
  exports.flattenJsonForAppend = flattenJsonForAppend
519
642
  exports.getUsageText = getUsageText
520
643
  exports.isJsonContentType = isJsonContentType
package/dist/index.d.cts CHANGED
@@ -50,6 +50,18 @@ declare function validateUrl(url: string): ValidationResult;
50
50
  */
51
51
  declare function normalizeBaseUrl(url: string): string;
52
52
  /**
53
+ * Build the full stream URL from a base URL and stream ID.
54
+ * Simply concatenates the stream ID to the base URL.
55
+ *
56
+ * Examples:
57
+ * buildStreamUrl("http://localhost:4437/v1/stream", "my-stream")
58
+ * => "http://localhost:4437/v1/stream/my-stream"
59
+ *
60
+ * buildStreamUrl("http://localhost:3002/v1/stream/my-group", "my-stream")
61
+ * => "http://localhost:3002/v1/stream/my-group/my-stream"
62
+ */
63
+ declare function buildStreamUrl(baseUrl: string, streamId: string): string;
64
+ /**
53
65
  * Validate an authorization header value.
54
66
  * Returns valid=true for any non-empty value, but includes a warning
55
67
  * if the value doesn't match common auth schemes (Bearer, Basic, ApiKey, Token).
@@ -82,4 +94,4 @@ declare function buildHeaders(options: GlobalOptions): Record<string, string>;
82
94
  declare function getUsageText(): string;
83
95
 
84
96
  //#endregion
85
- export { GlobalOptions, ParsedWriteArgs, buildHeaders, flattenJsonForAppend, getUsageText, isJsonContentType, normalizeBaseUrl, parseGlobalOptions, parseWriteArgs, validateAuth, validateStreamId, validateUrl };
97
+ export { GlobalOptions, ParsedWriteArgs, buildHeaders, buildStreamUrl, flattenJsonForAppend, getUsageText, isJsonContentType, normalizeBaseUrl, parseGlobalOptions, parseWriteArgs, validateAuth, validateStreamId, validateUrl };
package/dist/index.d.ts CHANGED
@@ -50,6 +50,18 @@ declare function validateUrl(url: string): ValidationResult;
50
50
  */
51
51
  declare function normalizeBaseUrl(url: string): string;
52
52
  /**
53
+ * Build the full stream URL from a base URL and stream ID.
54
+ * Simply concatenates the stream ID to the base URL.
55
+ *
56
+ * Examples:
57
+ * buildStreamUrl("http://localhost:4437/v1/stream", "my-stream")
58
+ * => "http://localhost:4437/v1/stream/my-stream"
59
+ *
60
+ * buildStreamUrl("http://localhost:3002/v1/stream/my-group", "my-stream")
61
+ * => "http://localhost:3002/v1/stream/my-group/my-stream"
62
+ */
63
+ declare function buildStreamUrl(baseUrl: string, streamId: string): string;
64
+ /**
53
65
  * Validate an authorization header value.
54
66
  * Returns valid=true for any non-empty value, but includes a warning
55
67
  * if the value doesn't match common auth schemes (Bearer, Basic, ApiKey, Token).
@@ -82,4 +94,4 @@ declare function buildHeaders(options: GlobalOptions): Record<string, string>;
82
94
  declare function getUsageText(): string;
83
95
 
84
96
  //#endregion
85
- export { GlobalOptions, ParsedWriteArgs, buildHeaders, flattenJsonForAppend, getUsageText, isJsonContentType, normalizeBaseUrl, parseGlobalOptions, parseWriteArgs, validateAuth, validateStreamId, validateUrl };
97
+ export { GlobalOptions, ParsedWriteArgs, buildHeaders, buildStreamUrl, 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 { realpathSync } from "node:fs";
2
3
  import { STATUS_CODES } from "node:http";
3
4
  import { resolve } from "node:path";
4
5
  import { stderr, stdin, stdout } from "node:process";
@@ -29,6 +30,34 @@ function* flattenJsonForAppend(parsed) {
29
30
  //#endregion
30
31
  //#region src/parseWriteArgs.ts
31
32
  /**
33
+ * Extract a flag value from args, supporting both --flag=value and --flag value syntax.
34
+ * Returns { value, consumed } where consumed is the number of args used (0 if no match).
35
+ */
36
+ function extractFlagValue$1(args, index, flagName) {
37
+ const arg = args[index];
38
+ const prefix = `${flagName}=`;
39
+ if (arg.startsWith(prefix)) {
40
+ const value = arg.slice(prefix.length);
41
+ if (!value) throw new Error(`${flagName} requires a value`);
42
+ return {
43
+ value,
44
+ consumed: 1
45
+ };
46
+ }
47
+ if (arg === flagName) {
48
+ const value = args[index + 1];
49
+ if (!value || value.startsWith(`--`)) throw new Error(`${flagName} requires a value`);
50
+ return {
51
+ value,
52
+ consumed: 2
53
+ };
54
+ }
55
+ return {
56
+ value: null,
57
+ consumed: 0
58
+ };
59
+ }
60
+ /**
32
61
  * Parse write command arguments, extracting content-type flags and content.
33
62
  * @param args - Arguments after the stream_id (starting from index 2)
34
63
  * @returns Parsed content type and content string
@@ -49,11 +78,10 @@ function parseWriteArgs(args) {
49
78
  contentType = `application/json`;
50
79
  continue;
51
80
  }
52
- if (arg === `--content-type`) {
53
- const nextArg = args[i + 1];
54
- if (!nextArg || nextArg.startsWith(`--`)) throw new Error(`--content-type requires a value`);
55
- contentType = nextArg;
56
- i++;
81
+ const contentTypeResult = extractFlagValue$1(args, i, `--content-type`);
82
+ if (contentTypeResult.value !== null) {
83
+ contentType = contentTypeResult.value;
84
+ i += contentTypeResult.consumed - 1;
57
85
  continue;
58
86
  }
59
87
  if (arg.startsWith(`--`)) throw new Error(`unknown flag: ${arg}`);
@@ -104,6 +132,20 @@ function normalizeBaseUrl(url) {
104
132
  return url.replace(/\/+$/, ``);
105
133
  }
106
134
  /**
135
+ * Build the full stream URL from a base URL and stream ID.
136
+ * Simply concatenates the stream ID to the base URL.
137
+ *
138
+ * Examples:
139
+ * buildStreamUrl("http://localhost:4437/v1/stream", "my-stream")
140
+ * => "http://localhost:4437/v1/stream/my-stream"
141
+ *
142
+ * buildStreamUrl("http://localhost:3002/v1/stream/my-group", "my-stream")
143
+ * => "http://localhost:3002/v1/stream/my-group/my-stream"
144
+ */
145
+ function buildStreamUrl(baseUrl, streamId) {
146
+ return `${baseUrl}/${streamId}`;
147
+ }
148
+ /**
107
149
  * Validate an authorization header value.
108
150
  * Returns valid=true for any non-empty value, but includes a warning
109
151
  * if the value doesn't match common auth schemes (Bearer, Basic, ApiKey, Token).
@@ -151,9 +193,37 @@ function validateStreamId(streamId) {
151
193
 
152
194
  //#endregion
153
195
  //#region src/index.ts
154
- const STREAM_URL = process.env.STREAM_URL || `http://localhost:4437`;
196
+ const STREAM_URL = process.env.STREAM_URL || `http://localhost:4437/v1/stream`;
155
197
  const STREAM_AUTH = process.env.STREAM_AUTH;
156
198
  /**
199
+ * Extract a flag value from args, supporting both --flag=value and --flag value syntax.
200
+ * Returns { value, consumed } where consumed is the number of args used (0 if no match).
201
+ */
202
+ function extractFlagValue(args, index, flagName, example) {
203
+ const arg = args[index];
204
+ const prefix = `${flagName}=`;
205
+ if (arg.startsWith(prefix)) {
206
+ const value = arg.slice(prefix.length);
207
+ if (!value) throw new Error(`${flagName} requires a value\n Example: ${flagName}="${example}"`);
208
+ return {
209
+ value,
210
+ consumed: 1
211
+ };
212
+ }
213
+ if (arg === flagName) {
214
+ const value = args[index + 1];
215
+ if (!value || value.startsWith(`--`)) throw new Error(`${flagName} requires a value\n Example: ${flagName} "${example}"`);
216
+ return {
217
+ value,
218
+ consumed: 2
219
+ };
220
+ }
221
+ return {
222
+ value: null,
223
+ consumed: 0
224
+ };
225
+ }
226
+ /**
157
227
  * Parse global options (--url, --auth) from args.
158
228
  * Falls back to STREAM_URL/STREAM_AUTH env vars when flags not provided.
159
229
  * Returns the parsed options, remaining args, and any warnings.
@@ -164,22 +234,24 @@ function parseGlobalOptions(args) {
164
234
  const warnings = [];
165
235
  for (let i = 0; i < args.length; i++) {
166
236
  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);
237
+ const urlResult = extractFlagValue(args, i, `--url`, `http://localhost:4437`);
238
+ if (urlResult.value !== null) {
239
+ const urlValidation = validateUrl(urlResult.value);
171
240
  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);
241
+ options.url = normalizeBaseUrl(urlResult.value);
242
+ i += urlResult.consumed - 1;
243
+ continue;
244
+ }
245
+ const authResult = extractFlagValue(args, i, `--auth`, `Bearer my-token`);
246
+ if (authResult.value !== null) {
247
+ const authValidation = validateAuth(authResult.value);
178
248
  if (!authValidation.valid) throw new Error(authValidation.error);
179
249
  if (authValidation.warning) warnings.push(authValidation.warning);
180
- options.auth = value;
181
- i++;
182
- } else remainingArgs.push(arg);
250
+ options.auth = authResult.value;
251
+ i += authResult.consumed - 1;
252
+ continue;
253
+ }
254
+ remainingArgs.push(arg);
183
255
  }
184
256
  if (!options.url) {
185
257
  const urlValidation = validateUrl(STREAM_URL);
@@ -218,13 +290,15 @@ Global Options:
218
290
  --auth <value> Authorization header value (e.g., "Bearer my-token")
219
291
  --help, -h Show this help message
220
292
 
221
- Write Options:
222
- --content-type <type> Content-Type for the message (default: application/octet-stream)
223
- --json Write as JSON (input stored as single message)
293
+ Create/Write Options:
294
+ --content-type <type> Content-Type for the stream (default: application/octet-stream)
295
+ --json Shorthand for --content-type application/json
296
+
297
+ Write-only Options:
224
298
  --batch-json Write as JSON array of messages (each array element stored separately)
225
299
 
226
300
  Environment Variables:
227
- STREAM_URL Base URL of the stream server (default: http://localhost:4437)
301
+ STREAM_URL Base URL of the stream server (default: http://localhost:4437/v1/stream)
228
302
  STREAM_AUTH Authorization header value (overridden by --auth flag)
229
303
  `;
230
304
  }
@@ -232,13 +306,38 @@ function printUsage({ to = `stderr` } = {}) {
232
306
  const out = to === `stderr` ? stderr : stdout;
233
307
  out.write(getUsageText());
234
308
  }
235
- async function createStream(baseUrl, streamId, headers) {
236
- const url = `${baseUrl}/v1/stream/${streamId}`;
309
+ /**
310
+ * Parse create command arguments, extracting content-type flags.
311
+ * @param args - Arguments after the stream_id
312
+ * @returns Parsed content type
313
+ * @throws Error if --content-type is missing its value or if unknown flags are provided
314
+ */
315
+ function parseCreateArgs(args) {
316
+ let contentType = `application/octet-stream`;
317
+ for (let i = 0; i < args.length; i++) {
318
+ const arg = args[i];
319
+ if (arg === `--json`) {
320
+ contentType = `application/json`;
321
+ continue;
322
+ }
323
+ const contentTypeResult = extractFlagValue(args, i, `--content-type`, `application/json`);
324
+ if (contentTypeResult.value !== null) {
325
+ contentType = contentTypeResult.value;
326
+ i += contentTypeResult.consumed - 1;
327
+ continue;
328
+ }
329
+ if (arg.startsWith(`--`)) throw new Error(`unknown flag: ${arg}`);
330
+ throw new Error(`unexpected argument: ${arg}`);
331
+ }
332
+ return { contentType };
333
+ }
334
+ async function createStream(baseUrl, streamId, headers, contentType) {
335
+ const url = buildStreamUrl(baseUrl, streamId);
237
336
  try {
238
337
  await DurableStream.create({
239
338
  url,
240
339
  headers,
241
- contentType: `application/octet-stream`
340
+ contentType
242
341
  });
243
342
  console.log(`Stream created successfully: "${streamId}"`);
244
343
  console.log(` URL: ${url}`);
@@ -298,7 +397,7 @@ function processEscapeSequences(content) {
298
397
  return content.replace(/\\n/g, `\n`).replace(/\\t/g, `\t`).replace(/\\r/g, `\r`).replace(/\\\\/g, `\\`);
299
398
  }
300
399
  async function writeStream(baseUrl, streamId, contentType, batchJson, headers, content) {
301
- const url = `${baseUrl}/v1/stream/${streamId}`;
400
+ const url = buildStreamUrl(baseUrl, streamId);
302
401
  const isJson = isJsonContentType(contentType);
303
402
  let data;
304
403
  let source;
@@ -363,7 +462,7 @@ function formatBytes(bytes) {
363
462
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
364
463
  }
365
464
  async function readStream(baseUrl, streamId, headers) {
366
- const url = `${baseUrl}/v1/stream/${streamId}`;
465
+ const url = buildStreamUrl(baseUrl, streamId);
367
466
  try {
368
467
  const stream = new DurableStream({
369
468
  url,
@@ -378,7 +477,7 @@ async function readStream(baseUrl, streamId, headers) {
378
477
  }
379
478
  }
380
479
  async function deleteStream(baseUrl, streamId, headers) {
381
- const url = `${baseUrl}/v1/stream/${streamId}`;
480
+ const url = buildStreamUrl(baseUrl, streamId);
382
481
  try {
383
482
  const stream = new DurableStream({
384
483
  url,
@@ -435,7 +534,14 @@ async function main() {
435
534
  switch (command) {
436
535
  case `create`: {
437
536
  const streamId = getStreamId();
438
- await createStream(options.url, streamId, headers);
537
+ let createArgs;
538
+ try {
539
+ createArgs = parseCreateArgs(args.slice(2));
540
+ } catch (error) {
541
+ stderr.write(`Error: ${getErrorMessage(error)}\n`);
542
+ process.exit(1);
543
+ }
544
+ await createStream(options.url, streamId, headers, createArgs.contentType);
439
545
  break;
440
546
  }
441
547
  case `write`: {
@@ -468,21 +574,37 @@ async function main() {
468
574
  await deleteStream(options.url, streamId, headers);
469
575
  break;
470
576
  }
471
- default:
472
- if (command?.startsWith(`-`)) stderr.write(`Error: Unknown option "${command}"\n`);
577
+ default: {
578
+ const commandSpecificFlags = [
579
+ `--content-type`,
580
+ `--json`,
581
+ `--batch-json`
582
+ ];
583
+ if (command && commandSpecificFlags.includes(command)) {
584
+ stderr.write(`Error: "${command}" must come after the command and stream_id\n`);
585
+ stderr.write(` Example: durable-stream create <stream_id> ${command} ...\n`);
586
+ stderr.write(` Example: durable-stream write <stream_id> ${command} ...\n`);
587
+ } else if (command?.startsWith(`-`)) stderr.write(`Error: Unknown option "${command}"\n`);
473
588
  else {
474
589
  stderr.write(`Error: Unknown command "${command}"\n`);
475
590
  stderr.write(` Available commands: create, write, read, delete\n`);
476
591
  }
477
592
  stderr.write(` Run "durable-stream --help" for usage information\n`);
478
593
  process.exit(1);
594
+ }
479
595
  }
480
596
  }
481
597
  function isMainModule() {
482
598
  if (!process.argv[1]) return false;
483
- const scriptPath = resolve(process.argv[1]);
484
- const modulePath = fileURLToPath(import.meta.url);
485
- return scriptPath === modulePath;
599
+ try {
600
+ const scriptPath = realpathSync(resolve(process.argv[1]));
601
+ const modulePath = realpathSync(fileURLToPath(import.meta.url));
602
+ return scriptPath === modulePath;
603
+ } catch {
604
+ const scriptPath = resolve(process.argv[1]);
605
+ const modulePath = fileURLToPath(import.meta.url);
606
+ return scriptPath === modulePath;
607
+ }
486
608
  }
487
609
  if (isMainModule()) main().catch((error) => {
488
610
  stderr.write(`Fatal error: ${getErrorMessage(error)}\n`);
@@ -490,4 +612,4 @@ if (isMainModule()) main().catch((error) => {
490
612
  });
491
613
 
492
614
  //#endregion
493
- export { buildHeaders, flattenJsonForAppend, getUsageText, isJsonContentType, normalizeBaseUrl, parseGlobalOptions, parseWriteArgs, validateAuth, validateStreamId, validateUrl };
615
+ export { buildHeaders, buildStreamUrl, 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.8",
4
+ "version": "0.2.0",
5
5
  "author": "Durable Stream contributors",
6
6
  "bin": {
7
7
  "cli": "./dist/index.js",
@@ -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.7"
23
+ "@durable-streams/server": "0.2.0"
24
24
  },
25
25
  "engines": {
26
26
  "node": ">=18.0.0"