shopstack 0.2.5 → 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.
@@ -0,0 +1,126 @@
1
+ // Notifications carry no checkout authority. The caller always reads the
2
+ // authenticated canonical resource after this wait returns.
3
+ export function checkoutMonitor(client, checkoutId, WebSocketImplementation) {
4
+ let attempted = false;
5
+ let socket;
6
+ let unavailable = typeof WebSocketImplementation !== "function";
7
+ let opened = false;
8
+ let latest = 0;
9
+ let delivered = 0;
10
+ let wake;
11
+ const close = () => {
12
+ unavailable = true;
13
+ const current = socket;
14
+ socket = undefined;
15
+ try {
16
+ current?.close();
17
+ } catch {
18
+ /* Already closed or never opened. */
19
+ }
20
+ wake?.();
21
+ };
22
+ const connect = async (remainingMs) => {
23
+ attempted = true;
24
+ try {
25
+ const subscription = await client.subscribeToCheckoutUpdates(checkoutId, {
26
+ signal: AbortSignal.timeout(Math.max(1, Math.min(5_000, remainingMs))),
27
+ });
28
+ const expected = new URL(
29
+ `${client.baseUrl}/checkout/${encodeURIComponent(checkoutId)}/updates`,
30
+ );
31
+ expected.protocol = expected.protocol === "https:" ? "wss:" : "ws:";
32
+ if (
33
+ subscription?.socket_url !== expected.href ||
34
+ subscription.protocol !== "shopstack.v1" ||
35
+ typeof subscription.token !== "string" ||
36
+ subscription.token.length > 4096 ||
37
+ !/^cus_v1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/u.test(subscription.token) ||
38
+ !(Date.parse(subscription.expires_at) > Date.now())
39
+ ) {
40
+ unavailable = true;
41
+ return;
42
+ }
43
+ socket = new WebSocketImplementation(subscription.socket_url, [
44
+ subscription.protocol,
45
+ subscription.token,
46
+ ]);
47
+ latest = Number.isSafeInteger(subscription.presentation_revision)
48
+ ? subscription.presentation_revision
49
+ : 0;
50
+ socket.addEventListener("open", () => {
51
+ opened = true;
52
+ });
53
+ socket.addEventListener("close", close);
54
+ socket.addEventListener("error", close);
55
+ socket.addEventListener("message", (event) => {
56
+ if (typeof event.data !== "string" || event.data.length > 4096) return;
57
+ try {
58
+ const notification = JSON.parse(event.data);
59
+ if (
60
+ notification.type === "checkout_changed" &&
61
+ Number.isSafeInteger(notification.presentation_revision) &&
62
+ notification.presentation_revision > latest
63
+ ) {
64
+ latest = notification.presentation_revision;
65
+ wake?.();
66
+ }
67
+ } catch {
68
+ /* Malformed notifications cannot change checkout state. */
69
+ }
70
+ });
71
+ } catch (error) {
72
+ if (error?.status === 401 || error?.status === 403) throw error;
73
+ close();
74
+ }
75
+ };
76
+ return {
77
+ close,
78
+ async wait(after, remainingMs) {
79
+ const deadline = Date.now() + remainingMs;
80
+ if (!attempted && !unavailable) await connect(remainingMs);
81
+ const changed = () => latest > Math.max(after, delivered);
82
+ if (changed()) {
83
+ delivered = latest;
84
+ return;
85
+ }
86
+ if (!unavailable) {
87
+ await new Promise((resolve) => {
88
+ const finish = () => {
89
+ clearTimeout(timer);
90
+ wake = undefined;
91
+ resolve();
92
+ };
93
+ const timer = setTimeout(
94
+ () => {
95
+ if (!opened) close();
96
+ finish();
97
+ },
98
+ Math.max(
99
+ 1,
100
+ Math.min(opened ? 25_000 : 5_000, deadline - Date.now()),
101
+ ),
102
+ );
103
+ wake = () => {
104
+ if (unavailable || changed()) finish();
105
+ };
106
+ });
107
+ if (!unavailable || changed()) {
108
+ delivered = latest;
109
+ return;
110
+ }
111
+ }
112
+ const remaining = deadline - Date.now();
113
+ if (remaining < 1_000) {
114
+ await new Promise((resolve) =>
115
+ setTimeout(resolve, Math.max(0, remaining)),
116
+ );
117
+ return;
118
+ }
119
+ await client.waitForCheckoutUpdate(checkoutId, {
120
+ after,
121
+ wait: Math.min(25, Math.floor(remaining / 1_000)),
122
+ signal: AbortSignal.timeout(remaining),
123
+ });
124
+ },
125
+ };
126
+ }
package/src/cli.js CHANGED
@@ -1,60 +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
10
  shopstack signup
21
- # Create or resume an email-verified Personal or Developer profile.
22
11
  shopstack signup user --email EMAIL
23
- # Create a Personal profile without interactive account-type prompts.
24
12
  shopstack signup developer --email EMAIL
25
- # Create a Developer management profile without interactive prompts.
26
13
  shopstack signup resume SIGNUP_ID
27
- # Resume a known pending email-verification flow.
28
14
  shopstack users create --external-id ID [--profile NAME]
29
- # Create an independently scoped user from a Developer profile.
30
15
  shopstack profiles list
31
- # List local profiles without printing their credentials.
32
16
  shopstack profiles use NAME
33
- # Select the profile used by later commands.
34
17
 
35
18
  Payment connection:
36
19
  shopstack connect list
37
- # Inspect payment connections for the active user profile.
38
20
  shopstack connect link
39
- # 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
40
30
 
41
31
  Checkout:
42
32
  shopstack checkout create --file checkout.json
43
- # Create a checkout and return control immediately.
44
33
  shopstack checkout run --file checkout.json
45
- # Create, monitor, and complete a checkout interactively.
46
34
  shopstack checkout get CHECKOUT_ID
47
- # Read the canonical current checkout state.
48
- shopstack checkout view CHECKOUT_ID
49
- # 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
50
37
  shopstack checkout cancel CHECKOUT_ID
51
- # Cancel a checkout that has not reached a terminal state.
52
-
53
- Options:
54
- -h, --help
55
- # Show this command reference.
56
- -v, --version
57
- # Print the installed Shopstack CLI version.
58
38
  `;
59
39
 
60
40
  function parseOptions(args, allowed) {
@@ -86,6 +66,26 @@ function required(options, name) {
86
66
  return value;
87
67
  }
88
68
 
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.");
73
+ }
74
+ return value;
75
+ }
76
+
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}.`,
84
+ );
85
+ }
86
+ return value;
87
+ }
88
+
89
89
  function isAffirmative(value) {
90
90
  const answer = String(value).trim().toLowerCase();
91
91
  return answer === "y" || answer === "yes";
@@ -95,77 +95,6 @@ function writeJson(stream, value) {
95
95
  stream.write(`${JSON.stringify(value, null, 2)}\n`);
96
96
  }
97
97
 
98
- function delay(milliseconds) {
99
- return new Promise((resolve) => setTimeout(resolve, milliseconds));
100
- }
101
-
102
- async function openExternal(url) {
103
- const parsed = new URL(url);
104
- if (parsed.protocol !== "https:") {
105
- throw new Error("Only HTTPS connection URLs can be opened.");
106
- }
107
- const [command, args] =
108
- process.platform === "darwin"
109
- ? ["open", [url]]
110
- : process.platform === "win32"
111
- ? ["cmd.exe", ["/d", "/s", "/c", "start", "", url]]
112
- : ["xdg-open", [url]];
113
- await new Promise((resolve, reject) => {
114
- const child = spawn(command, args, { detached: true, stdio: "ignore" });
115
- child.once("error", reject);
116
- child.once("spawn", () => {
117
- child.unref();
118
- resolve();
119
- });
120
- });
121
- }
122
-
123
- async function connectLink(client, dependencies) {
124
- let result = await client.connect("link");
125
- if (
126
- result.connection_status !== "action_required" ||
127
- typeof result.connect_url !== "string"
128
- ) {
129
- return result;
130
- }
131
-
132
- dependencies.stderr.write(`Open Link: ${result.connect_url}\n`);
133
- if (typeof result.phrase === "string") {
134
- dependencies.stderr.write(`Confirmation phrase: ${result.phrase}\n`);
135
- }
136
- try {
137
- await dependencies.openExternal(result.connect_url);
138
- } catch {
139
- dependencies.stderr.write(
140
- "The browser could not open automatically. Open the URL shown above.\n",
141
- );
142
- }
143
- dependencies.stderr.write("Waiting for Link confirmation...\n");
144
-
145
- for (let attempt = 0; attempt < dependencies.linkPollAttempts; attempt += 1) {
146
- await dependencies.delay(dependencies.linkPollIntervalMs);
147
- result = await client.connect("link");
148
- if (
149
- result.connection_status === "active" &&
150
- result.checkout_ready === true
151
- ) {
152
- dependencies.stderr.write("✓ Link connected\n");
153
- return result;
154
- }
155
- if (
156
- result.connection_status !== "action_required" &&
157
- result.connection_status !== "connecting"
158
- ) {
159
- return result;
160
- }
161
- }
162
-
163
- dependencies.stderr.write(
164
- "Link setup is still pending. Run `shopstack connect link` to resume.\n",
165
- );
166
- return result;
167
- }
168
-
169
98
  async function readJsonFile(path) {
170
99
  return JSON.parse(await readFile(path, "utf8"));
171
100
  }
@@ -240,20 +169,21 @@ function sanitizedUserResult(result, profile) {
240
169
  return { api_key_saved: true, profile, user };
241
170
  }
242
171
 
243
- async function saveVerifiedSignup(result, profileName, configStore) {
172
+ async function saveVerifiedSignup(result, profileName, configStore, baseUrl) {
244
173
  await configStore.saveProfile(
245
174
  profileName,
246
175
  {
247
176
  accountId: result.account.id,
248
177
  apiKey: result.api_key,
249
178
  keyType: result.key_type,
179
+ ...(baseUrl === undefined ? {} : { baseUrl }),
250
180
  ...(result.user?.id === undefined ? {} : { userId: result.user.id }),
251
181
  },
252
182
  { activate: true },
253
183
  );
254
184
  }
255
185
 
256
- function signupPersistence(configStore, profileName) {
186
+ function signupPersistence(configStore, profileName, baseUrl) {
257
187
  return {
258
188
  async loadPending(input) {
259
189
  return typeof configStore.findPendingSignup === "function"
@@ -261,7 +191,11 @@ function signupPersistence(configStore, profileName) {
261
191
  : undefined;
262
192
  },
263
193
  async savePending(state) {
264
- await configStore.savePendingSignup({ ...state, profile: profileName });
194
+ await configStore.savePendingSignup({
195
+ ...state,
196
+ ...(baseUrl === undefined ? {} : { baseUrl }),
197
+ profile: profileName,
198
+ });
265
199
  },
266
200
  async completePending(state, result) {
267
201
  const pendingId = state.attemptId ?? state.signupId ?? state.id;
@@ -269,7 +203,7 @@ function signupPersistence(configStore, profileName) {
269
203
  await configStore.completePendingSignup(pendingId, profileName, result);
270
204
  return;
271
205
  }
272
- await saveVerifiedSignup(result, profileName, configStore);
206
+ await saveVerifiedSignup(result, profileName, configStore, baseUrl);
273
207
  await configStore.deletePendingSignup(pendingId);
274
208
  },
275
209
  async deletePending(state) {
@@ -316,9 +250,23 @@ async function reportSignupProgress(progress, stream) {
316
250
  }
317
251
  }
318
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
+
319
267
  async function activeClient(dependencies, requiredKind = "user") {
320
268
  const profile = await dependencies.configStore.activeProfile();
321
- const environmentKey = dependencies.env.SHOPSTACK_API_KEY;
269
+ const environmentKey = process.env.SHOPSTACK_API_KEY;
322
270
  const apiKey = environmentKey || profile?.apiKey;
323
271
  const keyType = environmentKey ? requiredKind : profile?.keyType;
324
272
  if (!apiKey)
@@ -329,7 +277,10 @@ async function activeClient(dependencies, requiredKind = "user") {
329
277
  return {
330
278
  client: dependencies.clientFactory({
331
279
  apiKey,
332
- baseUrl: dependencies.env.SHOPSTACK_API_URL,
280
+ baseUrl: resolveProfileBaseUrl(
281
+ environmentKey ? undefined : profile,
282
+ process.env.SHOPSTACK_API_URL,
283
+ ),
333
284
  }),
334
285
  profile,
335
286
  };
@@ -340,33 +291,19 @@ export async function runCli(args, supplied = {}) {
340
291
  clientFactory: (options) => new ShopstackClient(options),
341
292
  configStore: new ConfigStore(),
342
293
  confirm: undefined,
343
- delay,
344
- linkPollAttempts: LINK_POLL_ATTEMPTS,
345
- linkPollIntervalMs: LINK_POLL_INTERVAL_MS,
346
- openExternal,
347
294
  prompt: undefined,
348
295
  readJsonFile,
349
296
  secretPrompt: undefined,
350
297
  stderr: process.stderr,
351
298
  stdin: process.stdin,
352
299
  stdout: process.stdout,
353
- env: process.env,
354
300
  ...supplied,
355
301
  };
356
302
  const [group, action, ...rest] = args;
357
- if (
358
- group === undefined ||
359
- group === "help" ||
360
- group === "--help" ||
361
- group === "-h"
362
- ) {
303
+ if (group === undefined || group === "help" || group === "--help") {
363
304
  dependencies.stdout.write(HELP);
364
305
  return;
365
306
  }
366
- if (group === "--version" || group === "-v") {
367
- dependencies.stdout.write(`${VERSION}\n`);
368
- return;
369
- }
370
307
 
371
308
  if (group === "signup") {
372
309
  if (action === "resume") {
@@ -380,7 +317,7 @@ export async function runCli(args, supplied = {}) {
380
317
  );
381
318
  }
382
319
  const client = dependencies.clientFactory({
383
- baseUrl: dependencies.env.SHOPSTACK_API_URL,
320
+ baseUrl: resolveProfileBaseUrl(pending, process.env.SHOPSTACK_API_URL),
384
321
  });
385
322
  const result = await client.signup({
386
323
  accountType: pending.accountType,
@@ -390,6 +327,7 @@ export async function runCli(args, supplied = {}) {
390
327
  persistence: signupPersistence(
391
328
  dependencies.configStore,
392
329
  pending.profile,
330
+ client.baseUrl ?? pending.baseUrl,
393
331
  ),
394
332
  });
395
333
  writeJson(dependencies.stdout, {
@@ -445,7 +383,7 @@ export async function runCli(args, supplied = {}) {
445
383
  email = required(options, "email");
446
384
  }
447
385
  const client = dependencies.clientFactory({
448
- baseUrl: dependencies.env.SHOPSTACK_API_URL,
386
+ baseUrl: resolveProfileBaseUrl(resumable, process.env.SHOPSTACK_API_URL),
449
387
  });
450
388
  const profileName =
451
389
  options.profile ??
@@ -456,7 +394,11 @@ export async function runCli(args, supplied = {}) {
456
394
  email,
457
395
  onProgress: (progress) =>
458
396
  reportSignupProgress(progress, dependencies.stderr),
459
- persistence: signupPersistence(dependencies.configStore, profileName),
397
+ persistence: signupPersistence(
398
+ dependencies.configStore,
399
+ profileName,
400
+ client.baseUrl ?? resumable?.baseUrl,
401
+ ),
460
402
  });
461
403
  writeJson(dependencies.stdout, {
462
404
  ...sanitizedAccountResult(result),
@@ -492,6 +434,9 @@ export async function runCli(args, supplied = {}) {
492
434
  accountId: profile.accountId,
493
435
  apiKey: user.api_key,
494
436
  keyType: "user",
437
+ ...(profile.baseUrl === undefined
438
+ ? {}
439
+ : { baseUrl: profile.baseUrl }),
495
440
  userId: user.id,
496
441
  },
497
442
  { activate: true },
@@ -518,6 +463,7 @@ export async function runCli(args, supplied = {}) {
518
463
  accountId: profile.accountId,
519
464
  apiKey: result.api_key,
520
465
  keyType: "user",
466
+ ...(profile.baseUrl === undefined ? {} : { baseUrl: profile.baseUrl }),
521
467
  userId: result.id,
522
468
  },
523
469
  { activate: true },
@@ -552,7 +498,124 @@ export async function runCli(args, supplied = {}) {
552
498
  const result =
553
499
  action === "list"
554
500
  ? await client.listConnections()
555
- : 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
+ );
556
619
  writeJson(dependencies.stdout, result);
557
620
  return;
558
621
  }
@@ -574,11 +637,6 @@ export async function runCli(args, supplied = {}) {
574
637
  const { client } = await activeClient(dependencies, "user");
575
638
  const request = await dependencies.readJsonFile(required(options, "file"));
576
639
  const result = await client.runCheckout(request, {
577
- onCreated: (checkout) => {
578
- if (typeof checkout.live_view_url === "string") {
579
- dependencies.stderr.write(`Live view: ${checkout.live_view_url}\n`);
580
- }
581
- },
582
640
  onProgress: (checkout) => {
583
641
  const intent =
584
642
  checkout.intent?.name === undefined
@@ -624,18 +682,50 @@ export async function runCli(args, supplied = {}) {
624
682
  return;
625
683
  }
626
684
 
627
- if (
628
- group === "checkout" &&
629
- (action === "get" || action === "view" || action === "cancel")
630
- ) {
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")) {
631
723
  if (rest.length !== 1) throw new Error("A checkout ID is required.");
632
724
  const { client } = await activeClient(dependencies, "user");
633
725
  const result =
634
726
  action === "get"
635
727
  ? await client.getCheckout(rest[0])
636
- : action === "view"
637
- ? await client.createLiveView(rest[0])
638
- : await client.cancelCheckout(rest[0]);
728
+ : await client.cancelCheckout(rest[0]);
639
729
  writeJson(dependencies.stdout, result);
640
730
  return;
641
731
  }
@@ -643,4 +733,4 @@ export async function runCli(args, supplied = {}) {
643
733
  throw new Error("Unknown command. Run `shopstack help`.");
644
734
  }
645
735
 
646
- export { HELP, VERSION };
736
+ export { HELP };