shopstack 0.2.6 → 0.3.1

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