shopstack 0.2.6 → 0.3.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/src/cli.js CHANGED
@@ -1,54 +1,40 @@
1
- import { spawn } from "node:child_process";
2
1
  import { readFile } from "node:fs/promises";
3
2
  import { createInterface } from "node:readline/promises";
4
3
 
5
4
  import { ShopstackClient } from "./client.js";
6
- import { ConfigStore } from "./config.js";
5
+ import { ConfigStore, resolveProfileBaseUrl } from "./config.js";
7
6
 
8
- const VERSION = JSON.parse(
9
- await readFile(new URL("../package.json", import.meta.url), "utf8"),
10
- ).version;
11
- const LINK_POLL_ATTEMPTS = 60;
12
- const LINK_POLL_INTERVAL_MS = 5_000;
13
-
14
- const HELP = `Shopstack CLI ${VERSION}
15
-
16
- Usage:
17
- shopstack <command> [options]
7
+ const HELP = `Shopstack
18
8
 
19
9
  Account setup:
20
- shopstack login [--email EMAIL] [--profile NAME] [--account-type personal|developer]
21
- # Log in, or create the requested account type when the email is new.
10
+ shopstack signup
11
+ shopstack signup user --email EMAIL
12
+ shopstack signup developer --email EMAIL
13
+ shopstack signup resume SIGNUP_ID
22
14
  shopstack users create --external-id ID [--profile NAME]
23
- # Create an independently scoped user from a Developer profile.
24
15
  shopstack profiles list
25
- # List local profiles without printing their credentials.
26
16
  shopstack profiles use NAME
27
- # Select the profile used by later commands.
28
17
 
29
18
  Payment connection:
30
19
  shopstack connect list
31
- # Inspect payment connections for the active user profile.
32
20
  shopstack connect link
33
- # Connect Link Agentic Wallet in the trusted browser flow.
21
+
22
+ Reservation:
23
+ shopstack reservation create --file reservation.json
24
+ shopstack reservation run --file reservation.json
25
+ shopstack reservation get RESERVATION_ID
26
+ shopstack reservation options RESERVATION_ID [--offset N] [--limit N]
27
+ shopstack reservation option RESERVATION_ID OPTION_ID
28
+ shopstack reservation message RESERVATION_ID --revision N --content TEXT
29
+ shopstack reservation cancel RESERVATION_ID --revision N
34
30
 
35
31
  Checkout:
36
32
  shopstack checkout create --file checkout.json
37
- # Create a checkout and return control immediately.
38
33
  shopstack checkout run --file checkout.json
39
- # Create, monitor, and complete a checkout interactively.
40
34
  shopstack checkout get CHECKOUT_ID
41
- # Read the canonical current checkout state.
42
- shopstack checkout view CHECKOUT_ID
43
- # Replace and return the active checkout's private owner live-view URL.
35
+ shopstack checkout updates CHECKOUT_ID --after N [--wait SECONDS]
36
+ shopstack checkout live-view CHECKOUT_ID
44
37
  shopstack checkout cancel CHECKOUT_ID
45
- # Cancel a checkout that has not reached a terminal state.
46
-
47
- Options:
48
- -h, --help
49
- # Show this command reference.
50
- -v, --version
51
- # Print the installed Shopstack CLI version.
52
38
  `;
53
39
 
54
40
  function parseOptions(args, allowed) {
@@ -80,79 +66,33 @@ function required(options, name) {
80
66
  return value;
81
67
  }
82
68
 
83
- function writeJson(stream, value) {
84
- stream.write(`${JSON.stringify(value, null, 2)}\n`);
85
- }
86
-
87
- function delay(milliseconds) {
88
- return new Promise((resolve) => setTimeout(resolve, milliseconds));
89
- }
90
-
91
- async function openExternal(url) {
92
- const parsed = new URL(url);
93
- if (parsed.protocol !== "https:") {
94
- throw new Error("Only HTTPS connection URLs can be opened.");
69
+ function requiredRevision(options) {
70
+ const value = Number(required(options, "revision"));
71
+ if (!Number.isSafeInteger(value) || value < 1) {
72
+ throw new Error("Option --revision must be a positive integer.");
95
73
  }
96
- const [command, args] =
97
- process.platform === "darwin"
98
- ? ["open", [url]]
99
- : process.platform === "win32"
100
- ? ["cmd.exe", ["/d", "/s", "/c", "start", "", url]]
101
- : ["xdg-open", [url]];
102
- await new Promise((resolve, reject) => {
103
- const child = spawn(command, args, { detached: true, stdio: "ignore" });
104
- child.once("error", reject);
105
- child.once("spawn", () => {
106
- child.unref();
107
- resolve();
108
- });
109
- });
74
+ return value;
110
75
  }
111
76
 
112
- async function connectLink(client, dependencies) {
113
- let result = await client.connect("link");
114
- if (
115
- result.connection_status !== "action_required" ||
116
- typeof result.connect_url !== "string"
117
- ) {
118
- return result;
119
- }
120
-
121
- dependencies.stderr.write(`Open Link: ${result.connect_url}\n`);
122
- if (typeof result.phrase === "string") {
123
- dependencies.stderr.write(`Confirmation phrase: ${result.phrase}\n`);
124
- }
125
- try {
126
- await dependencies.openExternal(result.connect_url);
127
- } catch {
128
- dependencies.stderr.write(
129
- "The browser could not open automatically. Open the URL shown above.\n",
77
+ function boundedIntegerOption(options, name, fallback, minimum, maximum) {
78
+ const raw = options[name];
79
+ if (raw === undefined) return fallback;
80
+ const value = Number(raw);
81
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
82
+ throw new Error(
83
+ `Option --${name} must be an integer from ${minimum} through ${maximum}.`,
130
84
  );
131
85
  }
132
- dependencies.stderr.write("Waiting for Link confirmation...\n");
133
-
134
- for (let attempt = 0; attempt < dependencies.linkPollAttempts; attempt += 1) {
135
- await dependencies.delay(dependencies.linkPollIntervalMs);
136
- result = await client.connect("link");
137
- if (
138
- result.connection_status === "active" &&
139
- result.checkout_ready === true
140
- ) {
141
- dependencies.stderr.write("✓ Link connected\n");
142
- return result;
143
- }
144
- if (
145
- result.connection_status !== "action_required" &&
146
- result.connection_status !== "connecting"
147
- ) {
148
- return result;
149
- }
150
- }
86
+ return value;
87
+ }
151
88
 
152
- dependencies.stderr.write(
153
- "Link setup is still pending. Run `shopstack connect link` to resume.\n",
154
- );
155
- return result;
89
+ function isAffirmative(value) {
90
+ const answer = String(value).trim().toLowerCase();
91
+ return answer === "y" || answer === "yes";
92
+ }
93
+
94
+ function writeJson(stream, value) {
95
+ stream.write(`${JSON.stringify(value, null, 2)}\n`);
156
96
  }
157
97
 
158
98
  async function readJsonFile(path) {
@@ -229,47 +169,52 @@ function sanitizedUserResult(result, profile) {
229
169
  return { api_key_saved: true, profile, user };
230
170
  }
231
171
 
232
- async function saveVerifiedLogin(result, profileName, configStore) {
172
+ async function saveVerifiedSignup(result, profileName, configStore, baseUrl) {
233
173
  await configStore.saveProfile(
234
174
  profileName,
235
175
  {
236
176
  accountId: result.account.id,
237
177
  apiKey: result.api_key,
238
178
  keyType: result.key_type,
179
+ ...(baseUrl === undefined ? {} : { baseUrl }),
239
180
  ...(result.user?.id === undefined ? {} : { userId: result.user.id }),
240
181
  },
241
182
  { activate: true },
242
183
  );
243
184
  }
244
185
 
245
- function loginPersistence(configStore, profileName) {
186
+ function signupPersistence(configStore, profileName, baseUrl) {
246
187
  return {
247
188
  async loadPending(input) {
248
- return typeof configStore.findPendingLogin === "function"
249
- ? configStore.findPendingLogin(input)
189
+ return typeof configStore.findPendingSignup === "function"
190
+ ? configStore.findPendingSignup(input)
250
191
  : undefined;
251
192
  },
252
193
  async savePending(state) {
253
- await configStore.savePendingLogin({ ...state, profile: profileName });
194
+ await configStore.savePendingSignup({
195
+ ...state,
196
+ ...(baseUrl === undefined ? {} : { baseUrl }),
197
+ profile: profileName,
198
+ });
254
199
  },
255
200
  async completePending(state, result) {
256
- const pendingId = state.attemptId ?? state.loginId ?? state.id;
257
- if (typeof configStore.completePendingLogin === "function") {
258
- await configStore.completePendingLogin(pendingId, profileName, result);
201
+ const pendingId = state.attemptId ?? state.signupId ?? state.id;
202
+ if (typeof configStore.completePendingSignup === "function") {
203
+ await configStore.completePendingSignup(pendingId, profileName, result);
259
204
  return;
260
205
  }
261
- await saveVerifiedLogin(result, profileName, configStore);
262
- await configStore.deletePendingLogin(pendingId);
206
+ await saveVerifiedSignup(result, profileName, configStore, baseUrl);
207
+ await configStore.deletePendingSignup(pendingId);
263
208
  },
264
209
  async deletePending(state) {
265
- await configStore.deletePendingLogin(
266
- state.attemptId ?? state.loginId ?? state.id,
210
+ await configStore.deletePendingSignup(
211
+ state.attemptId ?? state.signupId ?? state.id,
267
212
  );
268
213
  },
269
214
  };
270
215
  }
271
216
 
272
- async function reportLoginProgress(progress, stream) {
217
+ async function reportSignupProgress(progress, stream) {
273
218
  switch (progress.state) {
274
219
  case "email_sent":
275
220
  stream.write("Verification email sent.\n");
@@ -281,21 +226,23 @@ async function reportLoginProgress(progress, stream) {
281
226
  stream.write("✓ Email verified\n");
282
227
  return;
283
228
  case "complete":
284
- stream.write("✓ Shopstack login complete\n");
229
+ stream.write("✓ Shopstack account created\n");
285
230
  stream.write("✓ Credentials saved securely\n");
286
231
  return;
287
232
  case "expired":
288
- stream.write("Login expired. Start again to receive a new email.\n");
233
+ stream.write("Signup expired. Start again to receive a new email.\n");
289
234
  return;
290
235
  case "rate_limited":
291
- stream.write("Login is rate-limited. Retry after the indicated delay.\n");
236
+ stream.write(
237
+ "Signup is rate-limited. Retry after the indicated delay.\n",
238
+ );
292
239
  return;
293
240
  case "conflict":
294
- stream.write("Login retry state conflicted and was cleared.\n");
241
+ stream.write("Signup retry state conflicted and was cleared.\n");
295
242
  return;
296
243
  case "retryable_failure":
297
244
  stream.write(
298
- "Login paused after a retryable failure. Run login again to resume.\n",
245
+ "Signup paused after a retryable failure. Run signup again to resume.\n",
299
246
  );
300
247
  return;
301
248
  default:
@@ -303,19 +250,37 @@ async function reportLoginProgress(progress, stream) {
303
250
  }
304
251
  }
305
252
 
253
+ function reportReservationTurn(reservation, stream) {
254
+ const turn = reservation.turn;
255
+ if (typeof turn?.message === "string") {
256
+ stream.write(`${turn.message}\n`);
257
+ }
258
+ const details =
259
+ turn?.locations ??
260
+ turn?.options ??
261
+ turn?.confirmation_prompt ??
262
+ turn?.confirmation ??
263
+ turn?.cancellation;
264
+ if (details !== undefined) writeJson(stream, details);
265
+ }
266
+
306
267
  async function activeClient(dependencies, requiredKind = "user") {
307
268
  const profile = await dependencies.configStore.activeProfile();
308
- const environmentKey = dependencies.env.SHOPSTACK_API_KEY;
269
+ const environmentKey = process.env.SHOPSTACK_API_KEY;
309
270
  const apiKey = environmentKey || profile?.apiKey;
310
271
  const keyType = environmentKey ? requiredKind : profile?.keyType;
311
- if (!apiKey) throw new Error("No active Shopstack API key. Run login first.");
272
+ if (!apiKey)
273
+ throw new Error("No active Shopstack API key. Run signup first.");
312
274
  if (requiredKind && keyType !== requiredKind) {
313
275
  throw new Error(`This command requires an active ${requiredKind} profile.`);
314
276
  }
315
277
  return {
316
278
  client: dependencies.clientFactory({
317
279
  apiKey,
318
- baseUrl: dependencies.env.SHOPSTACK_API_URL,
280
+ baseUrl: resolveProfileBaseUrl(
281
+ environmentKey ? undefined : profile,
282
+ process.env.SHOPSTACK_API_URL,
283
+ ),
319
284
  }),
320
285
  profile,
321
286
  };
@@ -326,90 +291,158 @@ export async function runCli(args, supplied = {}) {
326
291
  clientFactory: (options) => new ShopstackClient(options),
327
292
  configStore: new ConfigStore(),
328
293
  confirm: undefined,
329
- delay,
330
- linkPollAttempts: LINK_POLL_ATTEMPTS,
331
- linkPollIntervalMs: LINK_POLL_INTERVAL_MS,
332
- openExternal,
333
294
  prompt: undefined,
334
295
  readJsonFile,
335
296
  secretPrompt: undefined,
336
297
  stderr: process.stderr,
337
298
  stdin: process.stdin,
338
299
  stdout: process.stdout,
339
- env: process.env,
340
300
  ...supplied,
341
301
  };
342
302
  const [group, action, ...rest] = args;
343
- if (
344
- group === undefined ||
345
- group === "help" ||
346
- group === "--help" ||
347
- group === "-h"
348
- ) {
303
+ if (group === undefined || group === "help" || group === "--help") {
349
304
  dependencies.stdout.write(HELP);
350
305
  return;
351
306
  }
352
- if (group === "--version" || group === "-v") {
353
- dependencies.stdout.write(`${VERSION}\n`);
354
- return;
355
- }
356
307
 
357
- if (group === "login") {
308
+ if (group === "signup") {
309
+ if (action === "resume") {
310
+ if (rest.length !== 1) {
311
+ throw new Error("Use `shopstack signup resume SIGNUP_ID`.");
312
+ }
313
+ const pending = await dependencies.configStore.pendingSignup(rest[0]);
314
+ if (pending === undefined) {
315
+ throw new Error(
316
+ "That signup is not present in the local profile store.",
317
+ );
318
+ }
319
+ const client = dependencies.clientFactory({
320
+ baseUrl: resolveProfileBaseUrl(pending, process.env.SHOPSTACK_API_URL),
321
+ });
322
+ const result = await client.signup({
323
+ accountType: pending.accountType,
324
+ email: pending.email,
325
+ onProgress: (progress) =>
326
+ reportSignupProgress(progress, dependencies.stderr),
327
+ persistence: signupPersistence(
328
+ dependencies.configStore,
329
+ pending.profile,
330
+ client.baseUrl ?? pending.baseUrl,
331
+ ),
332
+ });
333
+ writeJson(dependencies.stdout, {
334
+ ...sanitizedAccountResult(result),
335
+ profile: pending.profile,
336
+ });
337
+ return;
338
+ }
339
+ if (action !== undefined && action !== "user" && action !== "developer") {
340
+ throw new Error(
341
+ "Use `shopstack signup user` or `shopstack signup developer`.",
342
+ );
343
+ }
344
+ const signupArgs = action === undefined ? [] : rest;
358
345
  const { options, positional } = parseOptions(
359
- args.slice(1),
360
- new Set(["account-type", "email", "profile"]),
346
+ signupArgs,
347
+ new Set(["email", "profile"]),
361
348
  );
362
- if (positional.length > 0) throw new Error("Unexpected login argument.");
363
- const profileName = options.profile ?? "default";
364
- const requestedAccountType = options["account-type"] ?? "personal";
365
- if (
366
- requestedAccountType !== "personal" &&
367
- requestedAccountType !== "developer"
368
- ) {
369
- throw new Error("Account type must be personal or developer.");
370
- }
349
+ if (positional.length > 0) throw new Error("Unexpected signup argument.");
371
350
  const pendingCandidates =
372
- typeof dependencies.configStore.pendingLogins === "function"
373
- ? await dependencies.configStore.pendingLogins()
351
+ action === undefined &&
352
+ typeof dependencies.configStore.pendingSignups === "function"
353
+ ? await dependencies.configStore.pendingSignups()
374
354
  : [];
375
- const matchingPending = pendingCandidates.filter(
376
- (pending) =>
377
- pending.profile === profileName &&
378
- (options.email === undefined ||
379
- pending.email.trim().toLowerCase() ===
380
- options.email.trim().toLowerCase()) &&
381
- (options["account-type"] === undefined ||
382
- pending.accountType === requestedAccountType),
383
- );
384
355
  const resumable =
385
- matchingPending.length === 1 ? matchingPending[0] : undefined;
386
- const accountType = resumable?.accountType ?? requestedAccountType;
387
- let email = resumable?.email ?? options.email;
356
+ pendingCandidates.length === 1 ? pendingCandidates[0] : undefined;
357
+ let accountType;
358
+ let email;
388
359
  if (resumable !== undefined) {
389
- dependencies.stderr.write("Resuming pending login.\n");
390
- }
391
- if (email === undefined) {
360
+ accountType = resumable.accountType;
361
+ email = resumable.email;
362
+ dependencies.stderr.write("Resuming pending signup.\n");
363
+ } else if (action === undefined) {
392
364
  email = String(await visiblePrompt("Email: ", dependencies)).trim();
365
+ if (email.length === 0) throw new Error("Email is required.");
366
+ const selected = String(
367
+ await visiblePrompt(
368
+ "Account type (Personal / Developer): ",
369
+ dependencies,
370
+ ),
371
+ )
372
+ .trim()
373
+ .toLowerCase();
374
+ if (selected === "personal" || selected === "user") {
375
+ accountType = "personal";
376
+ } else if (selected === "developer") {
377
+ accountType = "developer";
378
+ } else {
379
+ throw new Error("Account type must be Personal or Developer.");
380
+ }
381
+ } else {
382
+ accountType = action === "user" ? "personal" : "developer";
383
+ email = required(options, "email");
393
384
  }
394
- if (email.length === 0) throw new Error("Email is required.");
395
385
  const client = dependencies.clientFactory({
396
- baseUrl: dependencies.env.SHOPSTACK_API_URL,
386
+ baseUrl: resolveProfileBaseUrl(resumable, process.env.SHOPSTACK_API_URL),
397
387
  });
398
- const result = await client.login({
388
+ const profileName =
389
+ options.profile ??
390
+ resumable?.profile ??
391
+ (accountType === "developer" ? "developer" : "default");
392
+ const result = await client.signup({
399
393
  accountType,
400
394
  email,
401
395
  onProgress: (progress) =>
402
- reportLoginProgress(progress, dependencies.stderr),
403
- persistence: loginPersistence(dependencies.configStore, profileName),
396
+ reportSignupProgress(progress, dependencies.stderr),
397
+ persistence: signupPersistence(
398
+ dependencies.configStore,
399
+ profileName,
400
+ client.baseUrl ?? resumable?.baseUrl,
401
+ ),
404
402
  });
405
403
  writeJson(dependencies.stdout, {
406
404
  ...sanitizedAccountResult(result),
407
405
  profile: profileName,
408
406
  });
409
- if (result.key_type === "developer") {
407
+ if (accountType === "developer") {
410
408
  dependencies.stderr.write(
411
409
  "The developer management credential cannot run user checkouts. Create a user profile with `shopstack users create --external-id ID`.\n",
412
410
  );
411
+ if (
412
+ action === undefined &&
413
+ isAffirmative(
414
+ await visiblePrompt(
415
+ "Create the first developer-owned user now? (y/N): ",
416
+ dependencies,
417
+ ),
418
+ )
419
+ ) {
420
+ const externalId = String(
421
+ await visiblePrompt("User external ID: ", dependencies),
422
+ ).trim();
423
+ if (externalId.length === 0) {
424
+ throw new Error("User external ID is required.");
425
+ }
426
+ const { client: developerClient, profile } = await activeClient(
427
+ dependencies,
428
+ "developer",
429
+ );
430
+ const user = await developerClient.createUser({ externalId });
431
+ await dependencies.configStore.saveProfile(
432
+ externalId,
433
+ {
434
+ accountId: profile.accountId,
435
+ apiKey: user.api_key,
436
+ keyType: "user",
437
+ ...(profile.baseUrl === undefined
438
+ ? {}
439
+ : { baseUrl: profile.baseUrl }),
440
+ userId: user.id,
441
+ },
442
+ { activate: true },
443
+ );
444
+ writeJson(dependencies.stdout, sanitizedUserResult(user, externalId));
445
+ }
413
446
  }
414
447
  return;
415
448
  }
@@ -430,6 +463,7 @@ export async function runCli(args, supplied = {}) {
430
463
  accountId: profile.accountId,
431
464
  apiKey: result.api_key,
432
465
  keyType: "user",
466
+ ...(profile.baseUrl === undefined ? {} : { baseUrl: profile.baseUrl }),
433
467
  userId: result.id,
434
468
  },
435
469
  { activate: true },
@@ -464,7 +498,124 @@ export async function runCli(args, supplied = {}) {
464
498
  const result =
465
499
  action === "list"
466
500
  ? await client.listConnections()
467
- : await connectLink(client, dependencies);
501
+ : await client.connect("link");
502
+ writeJson(dependencies.stdout, result);
503
+ return;
504
+ }
505
+
506
+ if (group === "reservation" && action === "create") {
507
+ const { options, positional } = parseOptions(rest, new Set(["file"]));
508
+ if (positional.length > 0) {
509
+ throw new Error("Unexpected reservation argument.");
510
+ }
511
+ const { client } = await activeClient(dependencies, "user");
512
+ const result = await client.createReservation(
513
+ await dependencies.readJsonFile(required(options, "file")),
514
+ );
515
+ writeJson(dependencies.stdout, result);
516
+ return;
517
+ }
518
+
519
+ if (group === "reservation" && action === "run") {
520
+ const { options, positional } = parseOptions(rest, new Set(["file"]));
521
+ if (positional.length > 0) {
522
+ throw new Error("Unexpected reservation argument.");
523
+ }
524
+ const { client } = await activeClient(dependencies, "user");
525
+ const request = await dependencies.readJsonFile(required(options, "file"));
526
+ const result = await client.runReservation(request, {
527
+ onTurn: (reservation) =>
528
+ reportReservationTurn(reservation, dependencies.stderr),
529
+ respond: async (reservation) => {
530
+ if (reservation.status === "confirmation_required") {
531
+ const accepted = dependencies.confirm
532
+ ? await dependencies.confirm(
533
+ reservation.turn?.confirmation_prompt,
534
+ reservation,
535
+ )
536
+ : isAffirmative(
537
+ await visiblePrompt(
538
+ "Confirm this exact booking? [y/N] ",
539
+ dependencies,
540
+ ),
541
+ );
542
+ return accepted
543
+ ? "Yes, confirm this exact booking."
544
+ : "No, do not book this reservation.";
545
+ }
546
+ return String(await visiblePrompt("Your reply: ", dependencies)).trim();
547
+ },
548
+ });
549
+ writeJson(dependencies.stdout, result);
550
+ return;
551
+ }
552
+
553
+ if (group === "reservation" && action === "get") {
554
+ if (rest.length !== 1) throw new Error("A reservation ID is required.");
555
+ const { client } = await activeClient(dependencies, "user");
556
+ writeJson(dependencies.stdout, await client.getReservation(rest[0]));
557
+ return;
558
+ }
559
+
560
+ if (group === "reservation" && action === "options") {
561
+ const { options, positional } = parseOptions(
562
+ rest,
563
+ new Set(["limit", "offset"]),
564
+ );
565
+ if (positional.length !== 1) {
566
+ throw new Error("A reservation ID is required.");
567
+ }
568
+ const { client } = await activeClient(dependencies, "user");
569
+ writeJson(
570
+ dependencies.stdout,
571
+ await client.listReservationOptions(positional[0], {
572
+ limit: boundedIntegerOption(options, "limit", 6, 1, 6),
573
+ offset: boundedIntegerOption(options, "offset", 0, 0, 29),
574
+ }),
575
+ );
576
+ return;
577
+ }
578
+
579
+ if (group === "reservation" && action === "option") {
580
+ if (rest.length !== 2) {
581
+ throw new Error("A reservation ID and option ID are required.");
582
+ }
583
+ const { client } = await activeClient(dependencies, "user");
584
+ writeJson(
585
+ dependencies.stdout,
586
+ await client.getReservationOption(rest[0], rest[1]),
587
+ );
588
+ return;
589
+ }
590
+
591
+ if (group === "reservation" && action === "message") {
592
+ const { options, positional } = parseOptions(
593
+ rest,
594
+ new Set(["content", "revision"]),
595
+ );
596
+ if (positional.length !== 1) {
597
+ throw new Error("A reservation ID is required.");
598
+ }
599
+ const { client } = await activeClient(dependencies, "user");
600
+ const result = await client.sendReservationMessage(
601
+ positional[0],
602
+ required(options, "content"),
603
+ requiredRevision(options),
604
+ );
605
+ writeJson(dependencies.stdout, result);
606
+ return;
607
+ }
608
+
609
+ if (group === "reservation" && action === "cancel") {
610
+ const { options, positional } = parseOptions(rest, new Set(["revision"]));
611
+ if (positional.length !== 1) {
612
+ throw new Error("A reservation ID is required.");
613
+ }
614
+ const { client } = await activeClient(dependencies, "user");
615
+ const result = await client.cancelReservation(
616
+ positional[0],
617
+ requiredRevision(options),
618
+ );
468
619
  writeJson(dependencies.stdout, result);
469
620
  return;
470
621
  }
@@ -486,11 +637,6 @@ export async function runCli(args, supplied = {}) {
486
637
  const { client } = await activeClient(dependencies, "user");
487
638
  const request = await dependencies.readJsonFile(required(options, "file"));
488
639
  const result = await client.runCheckout(request, {
489
- onCreated: (checkout) => {
490
- if (typeof checkout.live_view_url === "string") {
491
- dependencies.stderr.write(`Live view: ${checkout.live_view_url}\n`);
492
- }
493
- },
494
640
  onProgress: (checkout) => {
495
641
  const intent =
496
642
  checkout.intent?.name === undefined
@@ -536,18 +682,50 @@ export async function runCli(args, supplied = {}) {
536
682
  return;
537
683
  }
538
684
 
539
- if (
540
- group === "checkout" &&
541
- (action === "get" || action === "view" || action === "cancel")
542
- ) {
685
+ if (group === "checkout" && action === "updates") {
686
+ const { options, positional } = parseOptions(
687
+ rest,
688
+ new Set(["after", "wait"]),
689
+ );
690
+ if (positional.length !== 1) throw new Error("A checkout ID is required.");
691
+ required(options, "after");
692
+ const after = boundedIntegerOption(
693
+ options,
694
+ "after",
695
+ undefined,
696
+ 0,
697
+ Number.MAX_SAFE_INTEGER,
698
+ );
699
+ const wait = boundedIntegerOption(options, "wait", 25, 1, 25);
700
+ const { client } = await activeClient(dependencies, "user");
701
+ writeJson(
702
+ dependencies.stdout,
703
+ (await client.waitForCheckoutUpdate(positional[0], { after, wait })) ?? {
704
+ unchanged: true,
705
+ },
706
+ );
707
+ return;
708
+ }
709
+
710
+ if (group === "checkout" && action === "live-view") {
711
+ const { positional } = parseOptions(rest, new Set());
712
+ if (positional.length !== 1) throw new Error("A checkout ID is required.");
713
+ const { client } = await activeClient(dependencies, "user");
714
+ const result = await client.createLiveView(positional[0]);
715
+ dependencies.stderr.write(
716
+ "This replacement invalidates previous live-view links and viewer sessions.\n",
717
+ );
718
+ writeJson(dependencies.stdout, result);
719
+ return;
720
+ }
721
+
722
+ if (group === "checkout" && (action === "get" || action === "cancel")) {
543
723
  if (rest.length !== 1) throw new Error("A checkout ID is required.");
544
724
  const { client } = await activeClient(dependencies, "user");
545
725
  const result =
546
726
  action === "get"
547
727
  ? await client.getCheckout(rest[0])
548
- : action === "view"
549
- ? await client.createLiveView(rest[0])
550
- : await client.cancelCheckout(rest[0]);
728
+ : await client.cancelCheckout(rest[0]);
551
729
  writeJson(dependencies.stdout, result);
552
730
  return;
553
731
  }
@@ -555,4 +733,4 @@ export async function runCli(args, supplied = {}) {
555
733
  throw new Error("Unknown command. Run `shopstack help`.");
556
734
  }
557
735
 
558
- export { HELP, VERSION };
736
+ export { HELP };