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