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/README.md +139 -55
- package/SKILL.md +60 -43
- package/bin/shopstack +1 -3
- package/package.json +13 -23
- package/specs/openapi.json +3322 -0
- package/src/checkout-monitor.js +126 -0
- package/src/cli.js +364 -183
- package/src/client.d.ts +285 -51
- package/src/client.js +225 -205
- package/src/config.d.ts +19 -10
- package/src/config.js +46 -34
- package/src/reservation-progress.js +132 -0
- package/LICENSE +0 -21
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
|
|
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
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
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
|
|
189
|
+
function signupPersistence(configStore, profileName, baseUrl) {
|
|
246
190
|
return {
|
|
247
191
|
async loadPending(input) {
|
|
248
|
-
return typeof configStore.
|
|
249
|
-
? configStore.
|
|
192
|
+
return typeof configStore.findPendingSignup === "function"
|
|
193
|
+
? configStore.findPendingSignup(input)
|
|
250
194
|
: undefined;
|
|
251
195
|
},
|
|
252
196
|
async savePending(state) {
|
|
253
|
-
await configStore.
|
|
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.
|
|
257
|
-
if (typeof configStore.
|
|
258
|
-
await configStore.
|
|
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
|
|
262
|
-
await configStore.
|
|
209
|
+
await saveVerifiedSignup(result, profileName, configStore, baseUrl);
|
|
210
|
+
await configStore.deletePendingSignup(pendingId);
|
|
263
211
|
},
|
|
264
212
|
async deletePending(state) {
|
|
265
|
-
await configStore.
|
|
266
|
-
state.attemptId ?? state.
|
|
213
|
+
await configStore.deletePendingSignup(
|
|
214
|
+
state.attemptId ?? state.signupId ?? state.id,
|
|
267
215
|
);
|
|
268
216
|
},
|
|
269
217
|
};
|
|
270
218
|
}
|
|
271
219
|
|
|
272
|
-
async function
|
|
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
|
|
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("
|
|
236
|
+
stream.write("Signup expired. Start again to receive a new email.\n");
|
|
289
237
|
return;
|
|
290
238
|
case "rate_limited":
|
|
291
|
-
stream.write(
|
|
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("
|
|
244
|
+
stream.write("Signup retry state conflicted and was cleared.\n");
|
|
295
245
|
return;
|
|
296
246
|
case "retryable_failure":
|
|
297
247
|
stream.write(
|
|
298
|
-
"
|
|
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 =
|
|
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)
|
|
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:
|
|
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
|
-
|
|
360
|
-
new Set(["
|
|
349
|
+
signupArgs,
|
|
350
|
+
new Set(["email", "profile"]),
|
|
361
351
|
);
|
|
362
|
-
if (positional.length > 0) throw new Error("Unexpected
|
|
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
|
-
|
|
373
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
let email
|
|
359
|
+
pendingCandidates.length === 1 ? pendingCandidates[0] : undefined;
|
|
360
|
+
let accountType;
|
|
361
|
+
let email;
|
|
388
362
|
if (resumable !== undefined) {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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:
|
|
389
|
+
baseUrl: resolveProfileBaseUrl(resumable, process.env.SHOPSTACK_API_URL),
|
|
397
390
|
});
|
|
398
|
-
const
|
|
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
|
-
|
|
403
|
-
persistence:
|
|
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 (
|
|
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
|
|
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
|
-
|
|
541
|
-
|
|
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
|
-
:
|
|
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
|
|
739
|
+
export { HELP };
|