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.
- package/README.md +117 -39
- package/SKILL.md +45 -32
- package/bin/shopstack +1 -3
- package/package.json +13 -23
- package/specs/openapi.json +3293 -0
- package/src/checkout-monitor.js +126 -0
- package/src/cli.js +232 -142
- package/src/client.d.ts +258 -26
- package/src/client.js +188 -173
- package/src/config.d.ts +7 -0
- package/src/config.js +12 -0
- package/src/reservation-progress.js +132 -0
- package/LICENSE +0 -21
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
|
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
|
-
|
|
629
|
-
|
|
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
|
-
:
|
|
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
|
|
736
|
+
export { HELP };
|