@tenderprompt/cli 0.1.33 → 0.1.34
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 +26 -5
- package/dist/index.js +1152 -149
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -31,6 +31,7 @@ function readCliPackageVersion() {
|
|
|
31
31
|
return UNKNOWN_CLI_VERSION;
|
|
32
32
|
}
|
|
33
33
|
const CLI_PACKAGE_VERSION = readCliPackageVersion();
|
|
34
|
+
const GIT_COMMIT_SHA_PATTERN = /^[0-9a-f]{40}$/i;
|
|
34
35
|
const ANALYTICS_AUTHORING_PLAYBOOK_ID = 'analytics-event-authoring';
|
|
35
36
|
const ANALYTICS_AUTHORING_COMMAND = 'tender app analytics authoring --json';
|
|
36
37
|
const ANALYTICS_AUTHORING_PLAYBOOK_COMMAND = `tender playbooks get ${ANALYTICS_AUTHORING_PLAYBOOK_ID} --json`;
|
|
@@ -89,7 +90,7 @@ const ANALYTICS_AGENT_GUIDANCE = {
|
|
|
89
90
|
'clear split between shopper-facing UI and operator-facing analytics',
|
|
90
91
|
],
|
|
91
92
|
};
|
|
92
|
-
const DEFAULT_TENDER_APP_SCAFFOLD_TEMPLATE = '
|
|
93
|
+
const DEFAULT_TENDER_APP_SCAFFOLD_TEMPLATE = 'server-backed';
|
|
93
94
|
class TenderCliUsageError extends Error {
|
|
94
95
|
constructor(message) {
|
|
95
96
|
super(message);
|
|
@@ -134,12 +135,14 @@ class TenderCliConnectorsApiError extends Error {
|
|
|
134
135
|
reasonCode;
|
|
135
136
|
status;
|
|
136
137
|
payload;
|
|
137
|
-
|
|
138
|
+
example;
|
|
139
|
+
constructor(message, reasonCode, status, payload = null, example = 'tender connectors list --profile account --json') {
|
|
138
140
|
super(message);
|
|
139
141
|
this.name = 'TenderCliConnectorsApiError';
|
|
140
142
|
this.reasonCode = reasonCode;
|
|
141
143
|
this.status = status;
|
|
142
144
|
this.payload = payload;
|
|
145
|
+
this.example = example;
|
|
143
146
|
}
|
|
144
147
|
}
|
|
145
148
|
const DEFAULT_PROFILE_NAME = 'default';
|
|
@@ -177,6 +180,12 @@ Commands:
|
|
|
177
180
|
tender connectors shopify verify
|
|
178
181
|
Exchange credentials for an Admin API access token
|
|
179
182
|
tender connectors bind Print an app.json binding for a connector instance
|
|
183
|
+
tender shopify workspace connectors list
|
|
184
|
+
List current-store Shopify connectors from a hosted workspace
|
|
185
|
+
tender shopify workspace link-runtime --connector <connector-id>
|
|
186
|
+
Link the hosted runtime to a Shopify connector
|
|
187
|
+
tender shopify workspace deploy-source
|
|
188
|
+
Deploy linked Shopify source from a hosted workspace
|
|
180
189
|
tender app list List Tender App records visible to the current token
|
|
181
190
|
tender app create --name <name>
|
|
182
191
|
Create a new Tender App in the current account
|
|
@@ -239,8 +248,8 @@ Commands:
|
|
|
239
248
|
Examples:
|
|
240
249
|
tender --version
|
|
241
250
|
tender capabilities --json
|
|
242
|
-
tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d
|
|
243
|
-
tender auth status --json
|
|
251
|
+
tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d --save-profile agent --json
|
|
252
|
+
tender auth status --profile agent --json
|
|
244
253
|
tender memory add --kind decision --summary "Kept auth in the host gateway" --json
|
|
245
254
|
tender memory handoff --reason "Preview publish is blocked" --summary "Need Tender support" --json
|
|
246
255
|
tender memory brief --json
|
|
@@ -254,15 +263,18 @@ Examples:
|
|
|
254
263
|
printf "$SHOPIFY_APP_AUTOMATION_TOKEN" | tender connectors shopify app-automation-token set conn_123 --token-stdin --expires-at 2026-08-22 --profile account --json
|
|
255
264
|
tender connectors shopify verify conn_123 --profile account --json
|
|
256
265
|
tender connectors bind SHOPIFY_CONNECTOR conn_123 --instance default --json
|
|
266
|
+
tender shopify workspace connectors list --json
|
|
267
|
+
tender shopify workspace link-runtime --connector conn_123 --json
|
|
268
|
+
tender shopify workspace deploy-source --json
|
|
257
269
|
tender app list --json
|
|
258
270
|
tender app create --name "Exit Intent Widget" --json
|
|
259
|
-
tender app create --name "
|
|
260
|
-
tender app create --name "
|
|
271
|
+
tender app create --name "Data Collection App" --init --dir ./data-collection-app --preview --json
|
|
272
|
+
tender app create --name "Product Page Survey Runtime" --init --dir ./product-survey-runtime --scaffold shopify-pdp-survey-runtime --preview --json
|
|
261
273
|
tender app update artifact_123 --name "Sale Widget" --json
|
|
262
274
|
tender app share create artifact_123 --expires 7d --claims 1 --json
|
|
263
275
|
tender app claim tok_share_123 --dir ./widget --confirm source-and-setup --json
|
|
264
|
-
tender app init artifact_123 --dir ./
|
|
265
|
-
tender app init artifact_123 --dir ./
|
|
276
|
+
tender app init artifact_123 --dir ./data-collection-app --preview --json
|
|
277
|
+
tender app init artifact_123 --dir ./product-survey-runtime --scaffold shopify-pdp-survey-runtime --preview --json
|
|
266
278
|
tender app validate artifact_123 --json
|
|
267
279
|
tender app build artifact_123 --commit $(git rev-parse HEAD) --json
|
|
268
280
|
tender app preview artifact_123 --commit $(git rev-parse HEAD) --stream --json
|
|
@@ -297,12 +309,13 @@ Commands:
|
|
|
297
309
|
|
|
298
310
|
Examples:
|
|
299
311
|
tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d
|
|
300
|
-
tender auth login --device --profile publish --artifact artifact_123 --ttl 7d --save-profile publish
|
|
312
|
+
tender auth login --device --profile publish --artifact artifact_123 --ttl 7d --save-profile publish --json
|
|
313
|
+
tender auth status --profile publish --json
|
|
301
314
|
tender auth login --device --profile create --shopify-store tender-prompt --json
|
|
302
315
|
tender auth status --json`;
|
|
303
316
|
}
|
|
304
317
|
function authLoginHelp() {
|
|
305
|
-
return `Usage: tender auth login --device [--base-url <url>] [--shopify-store <store>] [--shopify-app-url <url>] [--profile <edit-preview|publish|create|db-read>] [--artifact <artifact-id>] [--ttl <7d|14d|30d>] [--save-profile <name>] [--no-poll] [--json]
|
|
318
|
+
return `Usage: tender auth login --device [--base-url <url>] [--shopify-store <store>] [--shopify-app-url <url>] [--profile <edit-preview|publish|create|db-read>] [--artifact <artifact-id>] [--ttl <7d|14d|30d>] [--save-profile <name>] [--no-poll|--poll] [--json]
|
|
306
319
|
|
|
307
320
|
Options:
|
|
308
321
|
--device Use the OAuth device-code flow.
|
|
@@ -313,12 +326,14 @@ Options:
|
|
|
313
326
|
--artifact <artifact-id> Scope the token to one artifact. Required for normal edit-preview agent work.
|
|
314
327
|
--ttl <value> Token lifetime: 7d, 14d, or 30d. Defaults to 7d.
|
|
315
328
|
--save-profile <name> Local config profile name. Defaults to default, or shopify-<store> with Shopify approval.
|
|
316
|
-
--no-poll Print the verification URL and exit without waiting
|
|
317
|
-
--
|
|
329
|
+
--no-poll Print the verification URL, store the pending device auth, and exit without waiting.
|
|
330
|
+
--poll With --json, wait for approval and save the profile after printing the pending handoff to stderr.
|
|
331
|
+
--json Emit stable JSON for coding agents. Defaults to --no-poll unless --poll is provided.
|
|
318
332
|
|
|
319
333
|
Examples:
|
|
320
334
|
tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d
|
|
321
|
-
tender auth login --device --profile publish --artifact artifact_123 --ttl 7d --save-profile publish --json
|
|
335
|
+
tender auth login --device --profile publish --artifact artifact_123 --ttl 7d --save-profile publish --no-poll --json
|
|
336
|
+
tender auth status --profile publish --json
|
|
322
337
|
tender auth login --device --profile create --ttl 7d --save-profile account
|
|
323
338
|
tender auth login --device --profile create --shopify-store tender-prompt --json
|
|
324
339
|
tender auth login --device --profile db-read --artifact artifact_123 --ttl 7d --save-profile db`;
|
|
@@ -463,7 +478,34 @@ Examples:
|
|
|
463
478
|
tender connectors shopify source init conn_123 --dir ./shopify-app --client-id abc --profile account --json
|
|
464
479
|
tender connectors shopify source validate conn_123 --dir ./shopify-app --execute --json
|
|
465
480
|
tender connectors shopify source deploy conn_123 --dir ./shopify-app --confirm deploy --profile account --json
|
|
466
|
-
|
|
481
|
+
tender connectors bind SHOPIFY_CONNECTOR conn_123 --instance default --json`;
|
|
482
|
+
}
|
|
483
|
+
function shopifyWorkspaceHelp() {
|
|
484
|
+
return `Usage: tender shopify workspace <command> [--json]
|
|
485
|
+
|
|
486
|
+
Hosted Shopify workspace commands use the current terminal workspace identity.
|
|
487
|
+
They do not read Tender profiles, account-scoped tokens, Shopify client secrets,
|
|
488
|
+
or Shopify App Automation Tokens from the terminal.
|
|
489
|
+
|
|
490
|
+
Commands:
|
|
491
|
+
tender shopify workspace connectors list
|
|
492
|
+
List current-store Shopify connectors available to this runtime
|
|
493
|
+
tender shopify workspace link-runtime --connector <connector-id>
|
|
494
|
+
Link this runtime to a Shopify connector source project
|
|
495
|
+
tender shopify workspace validate-source
|
|
496
|
+
Validate the linked Shopify connector source
|
|
497
|
+
tender shopify workspace deploy-source
|
|
498
|
+
Deploy the linked Shopify connector source
|
|
499
|
+
|
|
500
|
+
Required environment:
|
|
501
|
+
TENDER_BASE_URL
|
|
502
|
+
TENDER_RUNTIME_ARTIFACT_ID
|
|
503
|
+
|
|
504
|
+
Examples:
|
|
505
|
+
tender shopify workspace connectors list --json
|
|
506
|
+
tender shopify workspace link-runtime --connector conn_123 --json
|
|
507
|
+
tender shopify workspace validate-source --json
|
|
508
|
+
tender shopify workspace deploy-source --json`;
|
|
467
509
|
}
|
|
468
510
|
function connectorsShopifyCreateHelp() {
|
|
469
511
|
return `Usage: tender connectors shopify create [--instance <id>] [--label <text>] [--app-name <text>] [--admin-api-version <version>] [--shop <myshopify-domain>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
@@ -564,10 +606,12 @@ SHOPIFY_APP_AUTOMATION_TOKEN is not set locally, the CLI requests the
|
|
|
564
606
|
connector's stored App Automation Token and injects it only into the spawned
|
|
565
607
|
Shopify CLI child process.
|
|
566
608
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
609
|
+
Inside hosted Shopify project workspaces, direct shopify app deploy is allowed
|
|
610
|
+
when Shopify CLI is already authenticated there. Otherwise use
|
|
611
|
+
tender shopify workspace deploy-source so Tender can use the connector's
|
|
612
|
+
stored App Automation Token server-side. Do not run this account-scoped command
|
|
613
|
+
with a hosted workspace artifact token. Never ask the merchant to paste token
|
|
614
|
+
material into the terminal.
|
|
571
615
|
|
|
572
616
|
Examples:
|
|
573
617
|
tender connectors shopify source deploy conn_123 --dir ./shopify-app --version connector-source-smoke --confirm deploy --profile account --json`;
|
|
@@ -687,14 +731,14 @@ Commands:
|
|
|
687
731
|
Examples:
|
|
688
732
|
tender app list --json
|
|
689
733
|
tender app create --name "Exit Intent Widget" --json
|
|
690
|
-
tender app create --name "
|
|
691
|
-
tender app create --name "
|
|
734
|
+
tender app create --name "Data Collection App" --init --dir ./data-collection-app --preview --json
|
|
735
|
+
tender app create --name "Product Page Survey Runtime" --init --dir ./product-survey-runtime --scaffold shopify-pdp-survey-runtime --preview --json
|
|
692
736
|
tender app update artifact_123 --name "Sale Widget" --json
|
|
693
737
|
tender app share create artifact_123 --expires 7d --claims 1 --json
|
|
694
738
|
tender app share inspect tok_share_123 --json
|
|
695
739
|
tender app claim tok_share_123 --dir ./widget --confirm source-and-setup --json
|
|
696
|
-
tender app init artifact_123 --dir ./
|
|
697
|
-
tender app init artifact_123 --dir ./
|
|
740
|
+
tender app init artifact_123 --dir ./data-collection-app --preview --json
|
|
741
|
+
tender app init artifact_123 --dir ./product-survey-runtime --scaffold shopify-pdp-survey-runtime --preview --json
|
|
698
742
|
tender app validate artifact_123 --json
|
|
699
743
|
tender app build artifact_123 --commit $(git rev-parse HEAD) --json
|
|
700
744
|
tender app preview artifact_123 --commit $(git rev-parse HEAD) --stream --json
|
|
@@ -1124,7 +1168,7 @@ Options:
|
|
|
1124
1168
|
--name <name> App name.
|
|
1125
1169
|
--init Initialize a local checkout after creating the app.
|
|
1126
1170
|
--dir <path> Local checkout directory for --init. Defaults to current directory.
|
|
1127
|
-
--scaffold <value> API-derived scaffold for empty app source: server-backed, shopify-pdp-survey-runtime, or none. Defaults to
|
|
1171
|
+
--scaffold <value> API-derived scaffold for empty app source: server-backed, shopify-pdp-survey-runtime, or none. Defaults to server-backed.
|
|
1128
1172
|
--no-scaffold Alias for --scaffold none.
|
|
1129
1173
|
--preview After --init writes and pushes initial source, request a preview build.
|
|
1130
1174
|
--remote <name> Git remote name for --init. Defaults to tender.
|
|
@@ -1139,8 +1183,8 @@ Options:
|
|
|
1139
1183
|
|
|
1140
1184
|
Examples:
|
|
1141
1185
|
tender app create --name "Exit Intent Widget" --json
|
|
1142
|
-
tender app create --name "
|
|
1143
|
-
tender app create --name "
|
|
1186
|
+
tender app create --name "Data Collection App" --init --dir ./data-collection-app --preview --json
|
|
1187
|
+
tender app create --name "Product Page Survey Runtime" --init --dir ./product-survey-runtime --scaffold shopify-pdp-survey-runtime --preview --json
|
|
1144
1188
|
tender app create --name "Exit Intent Widget" --profile account --json`;
|
|
1145
1189
|
}
|
|
1146
1190
|
function artifactsUpdateHelp() {
|
|
@@ -1162,7 +1206,7 @@ function artifactsInitHelp() {
|
|
|
1162
1206
|
Options:
|
|
1163
1207
|
--dir <path> Local checkout directory to create/update. Defaults to current directory.
|
|
1164
1208
|
--remote <name> Git remote name to add or update. Defaults to tender.
|
|
1165
|
-
--scaffold <value> API-derived scaffold for empty app source: server-backed, shopify-pdp-survey-runtime, or none. Defaults to
|
|
1209
|
+
--scaffold <value> API-derived scaffold for empty app source: server-backed, shopify-pdp-survey-runtime, or none. Defaults to server-backed.
|
|
1166
1210
|
--no-scaffold Alias for --scaffold none.
|
|
1167
1211
|
--preview Request a preview build for the initialized source commit.
|
|
1168
1212
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
@@ -1175,8 +1219,8 @@ Options:
|
|
|
1175
1219
|
|
|
1176
1220
|
Examples:
|
|
1177
1221
|
tender app init artifact_123 --dir ./widget --dry-run --json
|
|
1178
|
-
tender app init artifact_123 --dir ./
|
|
1179
|
-
tender app init artifact_123 --dir ./
|
|
1222
|
+
tender app init artifact_123 --dir ./data-collection-app --preview --json
|
|
1223
|
+
tender app init artifact_123 --dir ./product-survey-runtime --scaffold shopify-pdp-survey-runtime --json
|
|
1180
1224
|
tender app init artifact_123 --dir ./widget --force --json`;
|
|
1181
1225
|
}
|
|
1182
1226
|
function artifactsContextFetchHelp() {
|
|
@@ -1319,10 +1363,10 @@ Examples:
|
|
|
1319
1363
|
tender app git credential artifact_123 get --remote-url https://app.tenderprompt.com/git/artifacts/artifact_123.git`;
|
|
1320
1364
|
}
|
|
1321
1365
|
function previewRebuildHelp() {
|
|
1322
|
-
return `Usage: tender app preview rebuild <app-id> [--commit <sha>] [--base-url <url>] [--token <token>] [--profile <name>] [--stream] [--json]
|
|
1366
|
+
return `Usage: tender app preview rebuild <app-id> [--commit <sha-or-ref>] [--base-url <url>] [--token <token>] [--profile <name>] [--stream] [--json]
|
|
1323
1367
|
|
|
1324
1368
|
Options:
|
|
1325
|
-
--commit <sha>
|
|
1369
|
+
--commit <sha-or-ref> Rebuild preview only if the app workspace is at this Git commit. Git refs like HEAD are resolved locally.
|
|
1326
1370
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
1327
1371
|
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
1328
1372
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
@@ -1348,10 +1392,10 @@ Examples:
|
|
|
1348
1392
|
tender app validate artifact_123 --stream --json`;
|
|
1349
1393
|
}
|
|
1350
1394
|
function buildHelp() {
|
|
1351
|
-
return `Usage: tender app build <app-id> [--commit <sha>] [--base-url <url>] [--token <token>] [--profile <name>] [--stream] [--json]
|
|
1395
|
+
return `Usage: tender app build <app-id> [--commit <sha-or-ref>] [--base-url <url>] [--token <token>] [--profile <name>] [--stream] [--json]
|
|
1352
1396
|
|
|
1353
1397
|
Options:
|
|
1354
|
-
--commit <sha>
|
|
1398
|
+
--commit <sha-or-ref> Build only if the app workspace is at this Git commit. Git refs like HEAD are resolved locally.
|
|
1355
1399
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
1356
1400
|
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
1357
1401
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
@@ -1363,10 +1407,10 @@ Examples:
|
|
|
1363
1407
|
tender app build artifact_123 --stream --json`;
|
|
1364
1408
|
}
|
|
1365
1409
|
function previewStatusHelp() {
|
|
1366
|
-
return `Usage: tender app preview status <app-id> [--commit <sha> | --job <job-id>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
1410
|
+
return `Usage: tender app preview status <app-id> [--commit <sha-or-ref> | --job <job-id>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
1367
1411
|
|
|
1368
1412
|
Options:
|
|
1369
|
-
--commit <sha>
|
|
1413
|
+
--commit <sha-or-ref> Show the latest preview job for one artifact Git commit. Git refs like HEAD are resolved locally.
|
|
1370
1414
|
--job <job-id> Show one lifecycle job directly.
|
|
1371
1415
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
1372
1416
|
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
@@ -1378,10 +1422,10 @@ Examples:
|
|
|
1378
1422
|
tender app preview status artifact_123 --job job_123 --json`;
|
|
1379
1423
|
}
|
|
1380
1424
|
function previewWatchHelp() {
|
|
1381
|
-
return `Usage: tender app preview watch <app-id> [--commit <sha> | --job <job-id>] [--base-url <url>] [--token <token>] [--profile <name>] [--stream] [--json]
|
|
1425
|
+
return `Usage: tender app preview watch <app-id> [--commit <sha-or-ref> | --job <job-id>] [--base-url <url>] [--token <token>] [--profile <name>] [--stream] [--json]
|
|
1382
1426
|
|
|
1383
1427
|
Options:
|
|
1384
|
-
--commit <sha>
|
|
1428
|
+
--commit <sha-or-ref> Watch the latest preview job for one artifact Git commit. Git refs like HEAD are resolved locally.
|
|
1385
1429
|
--job <job-id> Watch one lifecycle job directly.
|
|
1386
1430
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
1387
1431
|
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
@@ -1394,12 +1438,12 @@ Examples:
|
|
|
1394
1438
|
tender app preview watch artifact_123 --job job_123 --stream --json`;
|
|
1395
1439
|
}
|
|
1396
1440
|
function publishHelp() {
|
|
1397
|
-
return `Usage: tender app publish <app-id> (--dry-run | --confirm publish) [--commit <sha>] [--message <text>] [--base-url <url>] [--token <token>] [--profile <name>] [--stream] [--json]
|
|
1441
|
+
return `Usage: tender app publish <app-id> (--dry-run | --confirm publish) [--commit <sha-or-ref>] [--message <text>] [--base-url <url>] [--token <token>] [--profile <name>] [--stream] [--json]
|
|
1398
1442
|
|
|
1399
1443
|
Options:
|
|
1400
1444
|
--dry-run Show the publish request without calling the API.
|
|
1401
1445
|
--confirm publish Required to call the production-facing publish API.
|
|
1402
|
-
--commit <sha>
|
|
1446
|
+
--commit <sha-or-ref> Publish only if the app workspace is at this Git commit. Git refs like HEAD are resolved locally.
|
|
1403
1447
|
--message <text> Human audit message returned in CLI output.
|
|
1404
1448
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
1405
1449
|
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
@@ -1592,6 +1636,7 @@ function tenderCliCapabilities() {
|
|
|
1592
1636
|
'tender app outbound-auth --help',
|
|
1593
1637
|
'tender connectors --help',
|
|
1594
1638
|
'tender connectors shopify source init --help',
|
|
1639
|
+
'tender shopify workspace --help',
|
|
1595
1640
|
],
|
|
1596
1641
|
},
|
|
1597
1642
|
workflows: [
|
|
@@ -1600,7 +1645,8 @@ function tenderCliCapabilities() {
|
|
|
1600
1645
|
when: 'Before reading or changing remote Tender App records.',
|
|
1601
1646
|
commands: [
|
|
1602
1647
|
'tender auth status --json',
|
|
1603
|
-
'tender auth login --device --profile edit-preview --artifact <artifact-id> --ttl 7d --json',
|
|
1648
|
+
'tender auth login --device --profile edit-preview --artifact <artifact-id> --ttl 7d --save-profile agent --json',
|
|
1649
|
+
'tender auth status --profile agent --json',
|
|
1604
1650
|
],
|
|
1605
1651
|
},
|
|
1606
1652
|
{
|
|
@@ -1635,16 +1681,46 @@ function tenderCliCapabilities() {
|
|
|
1635
1681
|
'Fetch shopify-connector-customer-metadata-widget when the requested job involves customer metafields, metaobjects, or profile writes.',
|
|
1636
1682
|
],
|
|
1637
1683
|
},
|
|
1684
|
+
{
|
|
1685
|
+
name: 'hosted_shopify_project_release',
|
|
1686
|
+
when: 'Inside a hosted Shopify project workspace after editing a runtime app and linked Shopify connector source, or when the merchant asks to deploy/release/publish the Shopify connector.',
|
|
1687
|
+
commands: [
|
|
1688
|
+
'tender shopify workspace connectors list --json',
|
|
1689
|
+
'tender shopify workspace link-runtime --connector <connector-id> --json',
|
|
1690
|
+
'cd /workspace/runtime-project && git push origin HEAD:main',
|
|
1691
|
+
'cd /workspace/runtime-project && commit=$(git rev-parse HEAD) && tender app publish "$TENDER_RUNTIME_ARTIFACT_ID" --commit "$commit" --confirm publish --stream --json',
|
|
1692
|
+
'cd /workspace/shopify-connector && git push origin HEAD:main',
|
|
1693
|
+
'cd /workspace/shopify-connector && tender shopify workspace validate-source --json',
|
|
1694
|
+
'cd /workspace/shopify-connector && tender shopify workspace deploy-source --json',
|
|
1695
|
+
],
|
|
1696
|
+
notes: [
|
|
1697
|
+
'If /workspace/shopify-connector is missing, list connectors, link the selected connector, then continue in /workspace/shopify-connector unless link-runtime explicitly returns reloadRequired:true.',
|
|
1698
|
+
'For connector selection or switching, use tender shopify workspace connectors list --json and tender shopify workspace link-runtime --connector <connector-id> --json; do not use tender connectors list or ask for a Shopify Client ID.',
|
|
1699
|
+
'When selecting by merchant-provided Shopify app name, prefer an exact normalized label/appName match before any partial match; do not choose a longer connector name that merely contains the requested name.',
|
|
1700
|
+
'After link-runtime succeeds, tender shopify workspace validate-source and deploy-source automatically use that selected connector.',
|
|
1701
|
+
'For a deploy-only connector request, skip npm install, npm run check, tender app doctor, and other runtime-project checks; run cd /workspace/shopify-connector && tender shopify workspace deploy-source --json.',
|
|
1702
|
+
'For a validate-only connector request, run cd /workspace/shopify-connector && tender shopify workspace validate-source --json.',
|
|
1703
|
+
'Only run runtime-project checks when the merchant asks to change, validate, preview, or publish the runtime app.',
|
|
1704
|
+
'Publish the Tender runtime first and use the pinned runtime URL from publish JSON to update the linked connector source before deploying Shopify.',
|
|
1705
|
+
'Use TENDER_SHOPIFY_CONNECTOR_ID or connector.json for Tender connector commands; shopify.app.toml contains the Shopify client_id, not the Tender connector identity.',
|
|
1706
|
+
'Do not run tender connectors shopify source validate or tender connectors shopify source deploy from hosted workspaces; those commands require an account-scoped Tender CLI token.',
|
|
1707
|
+
'Use tender shopify workspace validate-source and deploy-source so the connector stored App Automation Token stays server-side.',
|
|
1708
|
+
'If Shopify CLI is already authenticated in the hosted workspace, direct shopify app config validate and shopify app deploy are allowed.',
|
|
1709
|
+
'Never ask for Shopify App Automation Tokens, client secrets, Tender tokens, or provider keys in the terminal.',
|
|
1710
|
+
'Do not release by pushing Git tags; Tender runtime publish plus tender shopify workspace deploy-source are the release path.',
|
|
1711
|
+
'If tender shopify workspace deploy-source reports a missing or expired App Automation Token, stop and ask the merchant to refresh the Custom App automation token in Tender.',
|
|
1712
|
+
],
|
|
1713
|
+
},
|
|
1638
1714
|
{
|
|
1639
1715
|
name: 'create_new_app_with_preview',
|
|
1640
|
-
when: 'When the user wants a new
|
|
1716
|
+
when: 'When the user wants a new Tender App that is immediately visible.',
|
|
1641
1717
|
commands: [
|
|
1642
|
-
'tender app create --name "
|
|
1643
|
-
'tender app create --name
|
|
1718
|
+
'tender app create --name "Data Collection App" --init --dir ./data-collection-app --preview --json',
|
|
1719
|
+
'tender app create --name "Product Page Survey Runtime" --init --dir ./product-survey-runtime --scaffold shopify-pdp-survey-runtime --preview --json',
|
|
1644
1720
|
],
|
|
1645
1721
|
notes: [
|
|
1646
|
-
'create --init defaults to the
|
|
1647
|
-
'Pass --scaffold
|
|
1722
|
+
'create --init defaults to the server-backed scaffold when the new app has no source.',
|
|
1723
|
+
'Pass --scaffold shopify-pdp-survey-runtime only for the Shopify product page survey starter.',
|
|
1648
1724
|
'--preview requests a preview build for the initial source commit after the push.',
|
|
1649
1725
|
],
|
|
1650
1726
|
},
|
|
@@ -1739,6 +1815,7 @@ function tenderCliCapabilities() {
|
|
|
1739
1815
|
when: 'When inspecting app-local database tables from a scoped token.',
|
|
1740
1816
|
commands: [
|
|
1741
1817
|
'tender auth login --device --profile db-read --artifact <artifact-id> --ttl 7d --save-profile db --json',
|
|
1818
|
+
'tender auth status --profile db --json',
|
|
1742
1819
|
'tender app db capabilities <artifact-id> --profile db --json',
|
|
1743
1820
|
'tender app db tables <artifact-id> --env published --profile db --json',
|
|
1744
1821
|
'tender app db schema <artifact-id> --env published --table <table-name> --profile db --json',
|
|
@@ -1807,7 +1884,7 @@ function tenderCliCapabilities() {
|
|
|
1807
1884
|
},
|
|
1808
1885
|
{
|
|
1809
1886
|
command: 'tender app create --name <name> --init --dir <dir> --preview --json',
|
|
1810
|
-
purpose: 'Create a new Tender App, initialize the default
|
|
1887
|
+
purpose: 'Create a new Tender App, initialize the default server-backed source in a local checkout, push it, and request a preview build.',
|
|
1811
1888
|
requiresAuth: true,
|
|
1812
1889
|
writes: true,
|
|
1813
1890
|
dryRun: false,
|
|
@@ -2058,6 +2135,45 @@ function normalizeStoredProfile(value) {
|
|
|
2058
2135
|
...normalizeStoredShopifyContext(record),
|
|
2059
2136
|
};
|
|
2060
2137
|
}
|
|
2138
|
+
function normalizeStoredPendingDeviceAuthorization(value) {
|
|
2139
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
2140
|
+
return null;
|
|
2141
|
+
}
|
|
2142
|
+
const record = value;
|
|
2143
|
+
if (typeof record.baseUrl !== 'string' ||
|
|
2144
|
+
typeof record.deviceCode !== 'string' ||
|
|
2145
|
+
typeof record.userCode !== 'string' ||
|
|
2146
|
+
typeof record.verificationUri !== 'string' ||
|
|
2147
|
+
typeof record.verificationUriComplete !== 'string' ||
|
|
2148
|
+
typeof record.expiresAt !== 'number' ||
|
|
2149
|
+
typeof record.tokenProfile !== 'string' ||
|
|
2150
|
+
typeof record.ttl !== 'string') {
|
|
2151
|
+
return null;
|
|
2152
|
+
}
|
|
2153
|
+
let tokenProfile;
|
|
2154
|
+
let ttl;
|
|
2155
|
+
try {
|
|
2156
|
+
tokenProfile = parseTokenProfile(record.tokenProfile);
|
|
2157
|
+
ttl = parseTtl(record.ttl);
|
|
2158
|
+
}
|
|
2159
|
+
catch {
|
|
2160
|
+
return null;
|
|
2161
|
+
}
|
|
2162
|
+
const artifactId = typeof record.artifactId === 'string' || record.artifactId === null ? record.artifactId : undefined;
|
|
2163
|
+
return {
|
|
2164
|
+
baseUrl: normalizeBaseUrl(record.baseUrl),
|
|
2165
|
+
deviceCode: record.deviceCode,
|
|
2166
|
+
userCode: record.userCode,
|
|
2167
|
+
verificationUri: record.verificationUri,
|
|
2168
|
+
verificationUriComplete: record.verificationUriComplete,
|
|
2169
|
+
expiresAt: record.expiresAt,
|
|
2170
|
+
interval: typeof record.interval === 'number' ? record.interval : undefined,
|
|
2171
|
+
tokenProfile,
|
|
2172
|
+
artifactId,
|
|
2173
|
+
ttl,
|
|
2174
|
+
...normalizeStoredShopifyContext(record),
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2061
2177
|
function normalizeStoredShopifyContext(record) {
|
|
2062
2178
|
const shopifyAppUrl = typeof record.shopifyAppUrl === 'string' ? record.shopifyAppUrl : null;
|
|
2063
2179
|
if (shopifyAppUrl) {
|
|
@@ -2101,9 +2217,21 @@ function normalizeConfig(value) {
|
|
|
2101
2217
|
}
|
|
2102
2218
|
}
|
|
2103
2219
|
}
|
|
2220
|
+
const pendingDeviceAuthorizations = {};
|
|
2221
|
+
if (record.pendingDeviceAuthorizations &&
|
|
2222
|
+
typeof record.pendingDeviceAuthorizations === 'object' &&
|
|
2223
|
+
!Array.isArray(record.pendingDeviceAuthorizations)) {
|
|
2224
|
+
for (const [name, pendingAuthorization] of Object.entries(record.pendingDeviceAuthorizations)) {
|
|
2225
|
+
const normalized = normalizeStoredPendingDeviceAuthorization(pendingAuthorization);
|
|
2226
|
+
if (normalized) {
|
|
2227
|
+
pendingDeviceAuthorizations[name] = normalized;
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2104
2231
|
return {
|
|
2105
2232
|
defaultProfile: typeof record.defaultProfile === 'string' && record.defaultProfile ? record.defaultProfile : undefined,
|
|
2106
2233
|
profiles: Object.keys(profiles).length > 0 ? profiles : undefined,
|
|
2234
|
+
pendingDeviceAuthorizations: Object.keys(pendingDeviceAuthorizations).length > 0 ? pendingDeviceAuthorizations : undefined,
|
|
2107
2235
|
};
|
|
2108
2236
|
}
|
|
2109
2237
|
async function readConfig(env) {
|
|
@@ -2170,6 +2298,30 @@ async function resolveApiCredentials(input) {
|
|
|
2170
2298
|
profileName,
|
|
2171
2299
|
};
|
|
2172
2300
|
}
|
|
2301
|
+
async function resolveLifecycleApiCredentials(input) {
|
|
2302
|
+
const baseUrl = normalizeBaseUrl(input.baseUrl ?? input.env.TENDER_BASE_URL ?? DEFAULT_BASE_URL);
|
|
2303
|
+
if (!input.flagToken &&
|
|
2304
|
+
!input.profileName &&
|
|
2305
|
+
isHostedWorkspaceInternalApiBaseUrl(baseUrl)) {
|
|
2306
|
+
return {
|
|
2307
|
+
baseUrl,
|
|
2308
|
+
token: null,
|
|
2309
|
+
source: 'hosted-workspace',
|
|
2310
|
+
profileName: null,
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2313
|
+
return await resolveApiCredentials(input);
|
|
2314
|
+
}
|
|
2315
|
+
function apiHeadersForCredentials(input) {
|
|
2316
|
+
const headers = {
|
|
2317
|
+
accept: input.accept ?? 'application/json',
|
|
2318
|
+
...(input.jsonBody ? { 'content-type': 'application/json' } : {}),
|
|
2319
|
+
};
|
|
2320
|
+
if (input.credentials.token) {
|
|
2321
|
+
headers.authorization = `Bearer ${input.credentials.token}`;
|
|
2322
|
+
}
|
|
2323
|
+
return headers;
|
|
2324
|
+
}
|
|
2173
2325
|
function parseProfileName(value, flag) {
|
|
2174
2326
|
if (!value || value.startsWith('-')) {
|
|
2175
2327
|
throw new TenderCliUsageError(`${flag} requires a profile name.`);
|
|
@@ -2560,7 +2712,7 @@ function parseBuildArgs(args) {
|
|
|
2560
2712
|
if (arg === '--commit') {
|
|
2561
2713
|
const value = args[index + 1];
|
|
2562
2714
|
if (!value || value.startsWith('-')) {
|
|
2563
|
-
throw new TenderCliUsageError('--commit requires a Git commit SHA.');
|
|
2715
|
+
throw new TenderCliUsageError('--commit requires a Git commit SHA or Git ref.');
|
|
2564
2716
|
}
|
|
2565
2717
|
commitSha = value;
|
|
2566
2718
|
index += 1;
|
|
@@ -2621,7 +2773,7 @@ function parsePreviewRebuildArgs(args) {
|
|
|
2621
2773
|
if (arg === '--commit') {
|
|
2622
2774
|
const value = args[index + 1];
|
|
2623
2775
|
if (!value || value.startsWith('-')) {
|
|
2624
|
-
throw new TenderCliUsageError('--commit requires a Git commit SHA.');
|
|
2776
|
+
throw new TenderCliUsageError('--commit requires a Git commit SHA or Git ref.');
|
|
2625
2777
|
}
|
|
2626
2778
|
commitSha = value;
|
|
2627
2779
|
index += 1;
|
|
@@ -2676,7 +2828,7 @@ function readPreviewLookupOptions(input) {
|
|
|
2676
2828
|
if (arg === '--commit') {
|
|
2677
2829
|
const value = input.args[index + 1];
|
|
2678
2830
|
if (!value || value.startsWith('-')) {
|
|
2679
|
-
throw new TenderCliUsageError('--commit requires a commit SHA.');
|
|
2831
|
+
throw new TenderCliUsageError('--commit requires a Git commit SHA or Git ref.');
|
|
2680
2832
|
}
|
|
2681
2833
|
commitSha = value;
|
|
2682
2834
|
index += 1;
|
|
@@ -2825,7 +2977,7 @@ function parsePublishArgs(args) {
|
|
|
2825
2977
|
if (arg === '--commit') {
|
|
2826
2978
|
const value = args[index + 1];
|
|
2827
2979
|
if (!value || value.startsWith('-')) {
|
|
2828
|
-
throw new TenderCliUsageError('--commit requires a commit SHA.');
|
|
2980
|
+
throw new TenderCliUsageError('--commit requires a Git commit SHA or Git ref.');
|
|
2829
2981
|
}
|
|
2830
2982
|
commitSha = value;
|
|
2831
2983
|
index += 1;
|
|
@@ -3320,7 +3472,7 @@ function parseArtifactsCreateArgs(args) {
|
|
|
3320
3472
|
}
|
|
3321
3473
|
if (!init) {
|
|
3322
3474
|
if (preview) {
|
|
3323
|
-
throw new TenderCliUsageError('--preview requires --init. Example: tender app create --name "
|
|
3475
|
+
throw new TenderCliUsageError('--preview requires --init. Example: tender app create --name "Data Collection App" --init --dir ./data-collection-app --preview --json');
|
|
3324
3476
|
}
|
|
3325
3477
|
if (force) {
|
|
3326
3478
|
throw new TenderCliUsageError('--force is only valid with --init.');
|
|
@@ -3867,6 +4019,58 @@ function parseConnectorsShopifySourceDeployArgs(args) {
|
|
|
3867
4019
|
}
|
|
3868
4020
|
return { command: 'connectors shopify source deploy', connectorId, dir, clientId, version, message, sourceControlUrl, allowDeletes, confirm, ...parsed };
|
|
3869
4021
|
}
|
|
4022
|
+
function parseShopifyWorkspaceSourceArgs(args, command) {
|
|
4023
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
4024
|
+
return { command: 'help', topic: command };
|
|
4025
|
+
}
|
|
4026
|
+
let json = false;
|
|
4027
|
+
for (const arg of args) {
|
|
4028
|
+
if (arg === '--json') {
|
|
4029
|
+
json = true;
|
|
4030
|
+
continue;
|
|
4031
|
+
}
|
|
4032
|
+
throw new TenderCliUsageError(`Unknown ${command} option: ${arg}`);
|
|
4033
|
+
}
|
|
4034
|
+
return { command, json };
|
|
4035
|
+
}
|
|
4036
|
+
function parseShopifyWorkspaceConnectorsListArgs(args) {
|
|
4037
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
4038
|
+
return { command: 'help', topic: 'shopify workspace connectors list' };
|
|
4039
|
+
}
|
|
4040
|
+
let json = false;
|
|
4041
|
+
for (const arg of args) {
|
|
4042
|
+
if (arg === '--json') {
|
|
4043
|
+
json = true;
|
|
4044
|
+
continue;
|
|
4045
|
+
}
|
|
4046
|
+
throw new TenderCliUsageError(`Unknown shopify workspace connectors list option: ${arg}`);
|
|
4047
|
+
}
|
|
4048
|
+
return { command: 'shopify workspace connectors list', json };
|
|
4049
|
+
}
|
|
4050
|
+
function parseShopifyWorkspaceLinkRuntimeArgs(args) {
|
|
4051
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
4052
|
+
return { command: 'help', topic: 'shopify workspace link-runtime' };
|
|
4053
|
+
}
|
|
4054
|
+
let connectorId = null;
|
|
4055
|
+
let json = false;
|
|
4056
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
4057
|
+
const arg = args[index];
|
|
4058
|
+
if (arg === '--json') {
|
|
4059
|
+
json = true;
|
|
4060
|
+
continue;
|
|
4061
|
+
}
|
|
4062
|
+
if (arg === '--connector') {
|
|
4063
|
+
connectorId = parseConnectorId(parseRequiredFlagValue(args[index + 1], '--connector'), 'tender shopify workspace link-runtime --connector conn_123 --json');
|
|
4064
|
+
index += 1;
|
|
4065
|
+
continue;
|
|
4066
|
+
}
|
|
4067
|
+
throw new TenderCliUsageError(`Unknown shopify workspace link-runtime option: ${arg}`);
|
|
4068
|
+
}
|
|
4069
|
+
if (!connectorId) {
|
|
4070
|
+
throw new TenderCliUsageError('--connector is required. Example: tender shopify workspace link-runtime --connector conn_123 --json');
|
|
4071
|
+
}
|
|
4072
|
+
return { command: 'shopify workspace link-runtime', connectorId, json };
|
|
4073
|
+
}
|
|
3870
4074
|
function parseConnectorsShopifyDeleteArgs(args) {
|
|
3871
4075
|
if (args.includes('--help') || args.includes('-h')) {
|
|
3872
4076
|
return { command: 'help', topic: 'connectors shopify delete' };
|
|
@@ -4522,6 +4726,7 @@ function parseAuthLoginArgs(args) {
|
|
|
4522
4726
|
let ttl = '7d';
|
|
4523
4727
|
let json = false;
|
|
4524
4728
|
let noPoll = false;
|
|
4729
|
+
let poll = false;
|
|
4525
4730
|
let pollTimeoutSeconds = 600;
|
|
4526
4731
|
for (let index = 0; index < args.length; index += 1) {
|
|
4527
4732
|
const arg = args[index];
|
|
@@ -4534,9 +4739,19 @@ function parseAuthLoginArgs(args) {
|
|
|
4534
4739
|
continue;
|
|
4535
4740
|
}
|
|
4536
4741
|
if (arg === '--no-poll') {
|
|
4742
|
+
if (poll) {
|
|
4743
|
+
throw new TenderCliUsageError('--no-poll cannot be combined with --poll.');
|
|
4744
|
+
}
|
|
4537
4745
|
noPoll = true;
|
|
4538
4746
|
continue;
|
|
4539
4747
|
}
|
|
4748
|
+
if (arg === '--poll') {
|
|
4749
|
+
if (noPoll) {
|
|
4750
|
+
throw new TenderCliUsageError('--poll cannot be combined with --no-poll.');
|
|
4751
|
+
}
|
|
4752
|
+
poll = true;
|
|
4753
|
+
continue;
|
|
4754
|
+
}
|
|
4540
4755
|
if (arg === '--base-url') {
|
|
4541
4756
|
baseUrl = parseBaseUrlFlag(args, index);
|
|
4542
4757
|
index += 1;
|
|
@@ -4583,6 +4798,10 @@ function parseAuthLoginArgs(args) {
|
|
|
4583
4798
|
continue;
|
|
4584
4799
|
}
|
|
4585
4800
|
if (arg === '--poll-timeout') {
|
|
4801
|
+
if (noPoll) {
|
|
4802
|
+
throw new TenderCliUsageError('--poll-timeout cannot be combined with --no-poll.');
|
|
4803
|
+
}
|
|
4804
|
+
poll = true;
|
|
4586
4805
|
pollTimeoutSeconds = parsePositiveIntegerFlag(args[index + 1], '--poll-timeout');
|
|
4587
4806
|
index += 1;
|
|
4588
4807
|
continue;
|
|
@@ -4612,7 +4831,7 @@ function parseAuthLoginArgs(args) {
|
|
|
4612
4831
|
artifactId,
|
|
4613
4832
|
ttl,
|
|
4614
4833
|
json,
|
|
4615
|
-
noPoll,
|
|
4834
|
+
noPoll: noPoll || (json && !poll),
|
|
4616
4835
|
pollTimeoutSeconds,
|
|
4617
4836
|
};
|
|
4618
4837
|
}
|
|
@@ -6947,6 +7166,30 @@ function parseTenderArgs(args) {
|
|
|
6947
7166
|
}
|
|
6948
7167
|
throw new TenderCliUsageError(`Unknown connectors command: ${args[1]}`);
|
|
6949
7168
|
}
|
|
7169
|
+
if (args[0] === 'shopify') {
|
|
7170
|
+
if (args[1] === undefined || args[1] === '--help' || args[1] === '-h') {
|
|
7171
|
+
return { command: 'help', topic: 'shopify' };
|
|
7172
|
+
}
|
|
7173
|
+
if (args[1] === 'workspace') {
|
|
7174
|
+
if (args[2] === undefined || args[2] === '--help' || args[2] === '-h') {
|
|
7175
|
+
return { command: 'help', topic: 'shopify workspace' };
|
|
7176
|
+
}
|
|
7177
|
+
if (args[2] === 'connectors' && args[3] === 'list') {
|
|
7178
|
+
return parseShopifyWorkspaceConnectorsListArgs(args.slice(4));
|
|
7179
|
+
}
|
|
7180
|
+
if (args[2] === 'link-runtime') {
|
|
7181
|
+
return parseShopifyWorkspaceLinkRuntimeArgs(args.slice(3));
|
|
7182
|
+
}
|
|
7183
|
+
if (args[2] === 'validate-source') {
|
|
7184
|
+
return parseShopifyWorkspaceSourceArgs(args.slice(3), 'shopify workspace validate-source');
|
|
7185
|
+
}
|
|
7186
|
+
if (args[2] === 'deploy-source') {
|
|
7187
|
+
return parseShopifyWorkspaceSourceArgs(args.slice(3), 'shopify workspace deploy-source');
|
|
7188
|
+
}
|
|
7189
|
+
throw new TenderCliUsageError(`Unknown shopify workspace command: ${args[2]}`);
|
|
7190
|
+
}
|
|
7191
|
+
throw new TenderCliUsageError(`Unknown shopify command: ${args[1]}`);
|
|
7192
|
+
}
|
|
6950
7193
|
if (args[0] === 'apps') {
|
|
6951
7194
|
throw new TenderCliUsageError('Unknown command: apps. Use `tender app ...` for Tender App workflows.');
|
|
6952
7195
|
}
|
|
@@ -7657,7 +7900,7 @@ async function requestPlaybooksApi(input) {
|
|
|
7657
7900
|
(typeof payload?.reasonCode === 'string' && payload.reasonCode) ||
|
|
7658
7901
|
`Playbooks request failed with HTTP ${response.status}.`;
|
|
7659
7902
|
const authHint = response.status === 401
|
|
7660
|
-
? '\nAuthenticate first: tender auth login --device --profile edit-preview --save-profile agent --ttl 7d --json'
|
|
7903
|
+
? '\nAuthenticate first: tender auth login --device --profile edit-preview --save-profile agent --ttl 7d --json, approve the URL, then run tender auth status --profile agent --json.'
|
|
7661
7904
|
: '';
|
|
7662
7905
|
throw new Error([message, `Reason: ${reasonCode}.`, 'Example: tender playbooks list --profile agent --json.'].join('\n') + authHint);
|
|
7663
7906
|
}
|
|
@@ -7665,13 +7908,16 @@ async function requestPlaybooksApi(input) {
|
|
|
7665
7908
|
}
|
|
7666
7909
|
async function requestConnectorsApi(input) {
|
|
7667
7910
|
const suffix = input.connectorsPath ? `/${input.connectorsPath.replace(/^\/+/, '')}` : '';
|
|
7911
|
+
const headers = {
|
|
7912
|
+
accept: 'application/json',
|
|
7913
|
+
...(input.body === undefined ? {} : { 'content-type': 'application/json' }),
|
|
7914
|
+
};
|
|
7915
|
+
if (input.token) {
|
|
7916
|
+
headers.authorization = `Bearer ${input.token}`;
|
|
7917
|
+
}
|
|
7668
7918
|
const response = await input.fetcher(`${input.baseUrl}/api/account/connectors${suffix}`, {
|
|
7669
7919
|
method: input.method,
|
|
7670
|
-
headers
|
|
7671
|
-
authorization: `Bearer ${input.token}`,
|
|
7672
|
-
accept: 'application/json',
|
|
7673
|
-
...(input.body === undefined ? {} : { 'content-type': 'application/json' }),
|
|
7674
|
-
},
|
|
7920
|
+
headers,
|
|
7675
7921
|
body: input.body === undefined ? undefined : JSON.stringify(input.body),
|
|
7676
7922
|
});
|
|
7677
7923
|
const payload = response.status === 204 ? { ok: true } : await parseJsonResponse(response);
|
|
@@ -7680,10 +7926,189 @@ async function requestConnectorsApi(input) {
|
|
|
7680
7926
|
const message = (typeof payload?.message === 'string' && payload.message) ||
|
|
7681
7927
|
(typeof payload?.reasonCode === 'string' && payload.reasonCode) ||
|
|
7682
7928
|
`Connector request failed with HTTP ${response.status}.`;
|
|
7683
|
-
|
|
7929
|
+
const example = input.hostedWorkspaceHint && reasonCode === 'account_scope_required'
|
|
7930
|
+
? 'Hosted workspace example: tender shopify workspace connectors list --json.'
|
|
7931
|
+
: 'Example: tender connectors list --profile account --json.';
|
|
7932
|
+
throw new TenderCliConnectorsApiError([message, `Reason: ${reasonCode}.`, example].join('\n'), reasonCode, response.status, payload ?? null, example.replace(/^Example:\s*/i, '').replace(/^Hosted workspace example:\s*/i, '').replace(/\.$/, ''));
|
|
7684
7933
|
}
|
|
7685
7934
|
return payload;
|
|
7686
7935
|
}
|
|
7936
|
+
function normalizeHostedWorkspaceBaseUrl(value) {
|
|
7937
|
+
const baseUrl = value?.trim().replace(/\/+$/, '') ?? '';
|
|
7938
|
+
if (!baseUrl) {
|
|
7939
|
+
return null;
|
|
7940
|
+
}
|
|
7941
|
+
try {
|
|
7942
|
+
const url = new URL(baseUrl);
|
|
7943
|
+
return url.protocol === 'https:' || url.protocol === 'http:' ? url.toString().replace(/\/+$/, '') : null;
|
|
7944
|
+
}
|
|
7945
|
+
catch {
|
|
7946
|
+
return null;
|
|
7947
|
+
}
|
|
7948
|
+
}
|
|
7949
|
+
function readHostedWorkspaceArtifactId(value) {
|
|
7950
|
+
const artifactId = value?.trim() ?? '';
|
|
7951
|
+
return /^artifact_[A-Za-z0-9_-]+$/.test(artifactId) ? artifactId : null;
|
|
7952
|
+
}
|
|
7953
|
+
function isHostedWorkspaceInternalApiBaseUrl(baseUrl) {
|
|
7954
|
+
try {
|
|
7955
|
+
return new URL(baseUrl).hostname === 'tender-api.tender.test';
|
|
7956
|
+
}
|
|
7957
|
+
catch {
|
|
7958
|
+
return false;
|
|
7959
|
+
}
|
|
7960
|
+
}
|
|
7961
|
+
function hostedWorkspaceApiHeaders(input) {
|
|
7962
|
+
return {
|
|
7963
|
+
accept: 'application/json',
|
|
7964
|
+
...(input.jsonBody ? { 'content-type': 'application/json' } : {}),
|
|
7965
|
+
};
|
|
7966
|
+
}
|
|
7967
|
+
function readHostedWorkspaceConnectorId(value) {
|
|
7968
|
+
const connectorId = value?.trim() ?? '';
|
|
7969
|
+
return /^conn_[A-Za-z0-9_-]+$/.test(connectorId) ? connectorId : null;
|
|
7970
|
+
}
|
|
7971
|
+
function errorCodeMatches(error, code) {
|
|
7972
|
+
return Boolean(error &&
|
|
7973
|
+
typeof error === 'object' &&
|
|
7974
|
+
'code' in error &&
|
|
7975
|
+
error.code === code);
|
|
7976
|
+
}
|
|
7977
|
+
function hostedWorkspaceSelectionFilePath(runtime) {
|
|
7978
|
+
const env = runtime.env ?? process.env;
|
|
7979
|
+
const explicitPath = env.TENDER_WORKSPACE_STATE_FILE?.trim();
|
|
7980
|
+
if (explicitPath) {
|
|
7981
|
+
return path.resolve(explicitPath);
|
|
7982
|
+
}
|
|
7983
|
+
const runtimeProjectDir = env.TENDER_RUNTIME_PROJECT_DIR?.trim();
|
|
7984
|
+
if (runtimeProjectDir && path.isAbsolute(runtimeProjectDir)) {
|
|
7985
|
+
return path.join(path.dirname(runtimeProjectDir), '.tender', 'shopify-workspace.json');
|
|
7986
|
+
}
|
|
7987
|
+
const cwd = process.cwd();
|
|
7988
|
+
if (cwd === '/workspace' || cwd.startsWith('/workspace/')) {
|
|
7989
|
+
return '/workspace/.tender/shopify-workspace.json';
|
|
7990
|
+
}
|
|
7991
|
+
return null;
|
|
7992
|
+
}
|
|
7993
|
+
async function writeHostedWorkspaceConnectorSelection(input, runtime) {
|
|
7994
|
+
const filePath = hostedWorkspaceSelectionFilePath(runtime);
|
|
7995
|
+
if (!filePath) {
|
|
7996
|
+
return;
|
|
7997
|
+
}
|
|
7998
|
+
await mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
7999
|
+
await writeFile(filePath, `${JSON.stringify({
|
|
8000
|
+
version: 1,
|
|
8001
|
+
runtimeArtifactId: input.runtimeArtifactId,
|
|
8002
|
+
connectorId: input.connectorId,
|
|
8003
|
+
selectedAt: Date.now(),
|
|
8004
|
+
}, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
8005
|
+
}
|
|
8006
|
+
async function readHostedWorkspaceConnectorSelection(runtimeArtifactId, runtime) {
|
|
8007
|
+
const env = runtime.env ?? process.env;
|
|
8008
|
+
const envConnectorId = readHostedWorkspaceConnectorId(env.TENDER_SHOPIFY_CONNECTOR_ID);
|
|
8009
|
+
if (envConnectorId) {
|
|
8010
|
+
return envConnectorId;
|
|
8011
|
+
}
|
|
8012
|
+
const filePath = hostedWorkspaceSelectionFilePath(runtime);
|
|
8013
|
+
if (!filePath) {
|
|
8014
|
+
return null;
|
|
8015
|
+
}
|
|
8016
|
+
let content;
|
|
8017
|
+
try {
|
|
8018
|
+
content = await readFile(filePath, 'utf8');
|
|
8019
|
+
}
|
|
8020
|
+
catch (error) {
|
|
8021
|
+
if (errorCodeMatches(error, 'ENOENT')) {
|
|
8022
|
+
return null;
|
|
8023
|
+
}
|
|
8024
|
+
throw error;
|
|
8025
|
+
}
|
|
8026
|
+
let parsed;
|
|
8027
|
+
try {
|
|
8028
|
+
parsed = JSON.parse(content);
|
|
8029
|
+
}
|
|
8030
|
+
catch {
|
|
8031
|
+
return null;
|
|
8032
|
+
}
|
|
8033
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
8034
|
+
return null;
|
|
8035
|
+
}
|
|
8036
|
+
const record = parsed;
|
|
8037
|
+
if (record.runtimeArtifactId !== runtimeArtifactId) {
|
|
8038
|
+
return null;
|
|
8039
|
+
}
|
|
8040
|
+
return readHostedWorkspaceConnectorId(typeof record.connectorId === 'string' ? record.connectorId : undefined);
|
|
8041
|
+
}
|
|
8042
|
+
function shopifyWorkspaceCommandApiPath(command) {
|
|
8043
|
+
return command === 'shopify workspace deploy-source' ? 'deploy' : 'validate';
|
|
8044
|
+
}
|
|
8045
|
+
async function requestShopifyWorkspaceSourceCommand(input) {
|
|
8046
|
+
const url = `${input.baseUrl}/api/account/connectors/${encodeURIComponent(input.connectorId)}/source/${shopifyWorkspaceCommandApiPath(input.command)}`;
|
|
8047
|
+
const response = await input.fetcher(url, {
|
|
8048
|
+
method: 'POST',
|
|
8049
|
+
headers: hostedWorkspaceApiHeaders({
|
|
8050
|
+
jsonBody: true,
|
|
8051
|
+
}),
|
|
8052
|
+
body: JSON.stringify({
|
|
8053
|
+
sourceArtifactId: input.sourceArtifactId,
|
|
8054
|
+
runtimeArtifactId: input.runtimeArtifactId,
|
|
8055
|
+
}),
|
|
8056
|
+
});
|
|
8057
|
+
const payload = response.status === 204 ? { ok: true } : await parseJsonResponse(response);
|
|
8058
|
+
if (payload) {
|
|
8059
|
+
return { status: response.status, payload };
|
|
8060
|
+
}
|
|
8061
|
+
return {
|
|
8062
|
+
status: response.status,
|
|
8063
|
+
payload: {
|
|
8064
|
+
ok: false,
|
|
8065
|
+
reasonCode: `http_${response.status}`,
|
|
8066
|
+
message: `Shopify workspace request failed with HTTP ${response.status}.`,
|
|
8067
|
+
},
|
|
8068
|
+
};
|
|
8069
|
+
}
|
|
8070
|
+
async function requestShopifyWorkspaceConnectorsList(input) {
|
|
8071
|
+
const params = new URLSearchParams({ runtimeArtifactId: input.runtimeArtifactId });
|
|
8072
|
+
const url = `${input.baseUrl}/api/account/connectors?${params.toString()}`;
|
|
8073
|
+
const response = await input.fetcher(url, {
|
|
8074
|
+
method: 'GET',
|
|
8075
|
+
headers: hostedWorkspaceApiHeaders({}),
|
|
8076
|
+
});
|
|
8077
|
+
const payload = response.status === 204 ? { ok: true } : await parseJsonResponse(response);
|
|
8078
|
+
if (payload) {
|
|
8079
|
+
return { status: response.status, payload };
|
|
8080
|
+
}
|
|
8081
|
+
return {
|
|
8082
|
+
status: response.status,
|
|
8083
|
+
payload: {
|
|
8084
|
+
ok: false,
|
|
8085
|
+
reasonCode: `http_${response.status}`,
|
|
8086
|
+
message: `Shopify workspace connectors request failed with HTTP ${response.status}.`,
|
|
8087
|
+
},
|
|
8088
|
+
};
|
|
8089
|
+
}
|
|
8090
|
+
async function requestShopifyWorkspaceLinkRuntime(input) {
|
|
8091
|
+
const url = `${input.baseUrl}/api/account/connectors/${encodeURIComponent(input.connectorId)}/runtime-links`;
|
|
8092
|
+
const response = await input.fetcher(url, {
|
|
8093
|
+
method: 'POST',
|
|
8094
|
+
headers: hostedWorkspaceApiHeaders({
|
|
8095
|
+
jsonBody: true,
|
|
8096
|
+
}),
|
|
8097
|
+
body: JSON.stringify({ runtimeArtifactId: input.runtimeArtifactId }),
|
|
8098
|
+
});
|
|
8099
|
+
const payload = response.status === 204 ? { ok: true } : await parseJsonResponse(response);
|
|
8100
|
+
if (payload) {
|
|
8101
|
+
return { status: response.status, payload };
|
|
8102
|
+
}
|
|
8103
|
+
return {
|
|
8104
|
+
status: response.status,
|
|
8105
|
+
payload: {
|
|
8106
|
+
ok: false,
|
|
8107
|
+
reasonCode: `http_${response.status}`,
|
|
8108
|
+
message: `Shopify workspace link request failed with HTTP ${response.status}.`,
|
|
8109
|
+
},
|
|
8110
|
+
};
|
|
8111
|
+
}
|
|
7687
8112
|
function dbCorrectedExample(artifactId, dbPath) {
|
|
7688
8113
|
if (dbPath === 'capabilities') {
|
|
7689
8114
|
return `tender app db capabilities ${artifactId} --json`;
|
|
@@ -8290,10 +8715,208 @@ async function planSourceFiles(input) {
|
|
|
8290
8715
|
}
|
|
8291
8716
|
return planned.sort((left, right) => left.path.localeCompare(right.path));
|
|
8292
8717
|
}
|
|
8718
|
+
function buildAuthStatusNextCommand(profileName) {
|
|
8719
|
+
return `tender auth status --profile ${profileName} --json`;
|
|
8720
|
+
}
|
|
8721
|
+
function buildAuthLoginRetryCommand(profileName, pendingAuthorization) {
|
|
8722
|
+
const parts = [
|
|
8723
|
+
'tender auth login --device',
|
|
8724
|
+
`--base-url ${pendingAuthorization.baseUrl}`,
|
|
8725
|
+
`--profile ${pendingAuthorization.tokenProfile}`,
|
|
8726
|
+
];
|
|
8727
|
+
if (pendingAuthorization.artifactId) {
|
|
8728
|
+
parts.push(`--artifact ${pendingAuthorization.artifactId}`);
|
|
8729
|
+
}
|
|
8730
|
+
parts.push(`--ttl ${pendingAuthorization.ttl}`, `--save-profile ${profileName}`);
|
|
8731
|
+
if (pendingAuthorization.shopifyAppUrl) {
|
|
8732
|
+
parts.push(`--shopify-app-url ${pendingAuthorization.shopifyAppUrl}`);
|
|
8733
|
+
}
|
|
8734
|
+
parts.push('--json');
|
|
8735
|
+
return parts.join(' ');
|
|
8736
|
+
}
|
|
8737
|
+
function pendingDeviceAuthorizationOutput(input) {
|
|
8738
|
+
return {
|
|
8739
|
+
ok: false,
|
|
8740
|
+
command: input.command,
|
|
8741
|
+
status: 'authorization_pending',
|
|
8742
|
+
authenticated: false,
|
|
8743
|
+
pending: true,
|
|
8744
|
+
baseUrl: input.baseUrl,
|
|
8745
|
+
profile: input.pendingAuthorization.tokenProfile,
|
|
8746
|
+
saveProfile: input.profileName,
|
|
8747
|
+
pendingProfile: input.profileName,
|
|
8748
|
+
artifactId: input.pendingAuthorization.artifactId ?? null,
|
|
8749
|
+
shopifyAppUrl: input.pendingAuthorization.shopifyAppUrl ?? null,
|
|
8750
|
+
shopifyStore: input.pendingAuthorization.shopifyStore ?? null,
|
|
8751
|
+
userCode: input.pendingAuthorization.userCode,
|
|
8752
|
+
verificationUri: input.pendingAuthorization.verificationUri,
|
|
8753
|
+
verificationUriComplete: input.pendingAuthorization.verificationUriComplete,
|
|
8754
|
+
expiresAt: formatExpiry(input.pendingAuthorization.expiresAt),
|
|
8755
|
+
interval: input.pendingAuthorization.interval ?? null,
|
|
8756
|
+
configPath: input.configPath,
|
|
8757
|
+
nextCommand: buildAuthStatusNextCommand(input.profileName),
|
|
8758
|
+
};
|
|
8759
|
+
}
|
|
8760
|
+
function storedProfileFromOAuthToken(input) {
|
|
8761
|
+
return {
|
|
8762
|
+
baseUrl: input.baseUrl,
|
|
8763
|
+
token: input.tokenPayload.access_token ?? '',
|
|
8764
|
+
tokenPrefix: input.tokenPayload.token_prefix,
|
|
8765
|
+
tenantId: input.tokenPayload.tenant_id,
|
|
8766
|
+
artifactId: input.tokenPayload.artifact_id,
|
|
8767
|
+
expiresAt: input.tokenPayload.expires_at,
|
|
8768
|
+
permissions: splitScope(input.tokenPayload.scope),
|
|
8769
|
+
...(input.shopifyAppUrl ? { shopifyAppUrl: input.shopifyAppUrl } : {}),
|
|
8770
|
+
...(input.shopifyStore ? { shopifyStore: input.shopifyStore } : {}),
|
|
8771
|
+
};
|
|
8772
|
+
}
|
|
8773
|
+
function authLoginSuccessOutput(input) {
|
|
8774
|
+
return {
|
|
8775
|
+
ok: true,
|
|
8776
|
+
command: input.command,
|
|
8777
|
+
baseUrl: input.baseUrl,
|
|
8778
|
+
profile: input.tokenProfile,
|
|
8779
|
+
savedProfile: input.saveProfile,
|
|
8780
|
+
tokenPrefix: input.tokenPayload.token_prefix ?? null,
|
|
8781
|
+
tenantId: input.tokenPayload.tenant_id ?? null,
|
|
8782
|
+
artifactId: input.tokenPayload.artifact_id ?? null,
|
|
8783
|
+
permissions: splitScope(input.tokenPayload.scope),
|
|
8784
|
+
expiresAt: formatExpiry(input.tokenPayload.expires_at),
|
|
8785
|
+
shopifyAppUrl: input.shopifyAppUrl,
|
|
8786
|
+
shopifyStore: input.shopifyStore,
|
|
8787
|
+
configPath: input.configPath,
|
|
8788
|
+
};
|
|
8789
|
+
}
|
|
8790
|
+
function configWithoutPendingDeviceAuthorization(config, profileName) {
|
|
8791
|
+
const pendingDeviceAuthorizations = { ...(config.pendingDeviceAuthorizations ?? {}) };
|
|
8792
|
+
delete pendingDeviceAuthorizations[profileName];
|
|
8793
|
+
return {
|
|
8794
|
+
...config,
|
|
8795
|
+
pendingDeviceAuthorizations: Object.keys(pendingDeviceAuthorizations).length > 0 ? pendingDeviceAuthorizations : undefined,
|
|
8796
|
+
};
|
|
8797
|
+
}
|
|
8798
|
+
async function completePendingDeviceAuthorization(input) {
|
|
8799
|
+
const configPath = readConfigPath(input.env);
|
|
8800
|
+
if (input.pendingAuthorization.expiresAt <= input.now()) {
|
|
8801
|
+
const updatedConfig = configWithoutPendingDeviceAuthorization(input.config, input.profileName);
|
|
8802
|
+
await writeConfig(input.env, updatedConfig);
|
|
8803
|
+
const expiredResult = {
|
|
8804
|
+
ok: false,
|
|
8805
|
+
command: input.command,
|
|
8806
|
+
status: 'authorization_expired',
|
|
8807
|
+
authenticated: false,
|
|
8808
|
+
pending: false,
|
|
8809
|
+
profile: input.profileName,
|
|
8810
|
+
configPath,
|
|
8811
|
+
nextCommand: buildAuthLoginRetryCommand(input.profileName, input.pendingAuthorization),
|
|
8812
|
+
};
|
|
8813
|
+
if (input.json) {
|
|
8814
|
+
input.io.stdout(JSON.stringify(expiredResult, null, 2));
|
|
8815
|
+
}
|
|
8816
|
+
else {
|
|
8817
|
+
input.io.stdout([
|
|
8818
|
+
'authenticated: false',
|
|
8819
|
+
`profile: ${input.profileName}`,
|
|
8820
|
+
'pending_device_auth: expired',
|
|
8821
|
+
`next: ${expiredResult.nextCommand}`,
|
|
8822
|
+
].join('\n'));
|
|
8823
|
+
}
|
|
8824
|
+
return 1;
|
|
8825
|
+
}
|
|
8826
|
+
const tokenResponse = await input.fetcher(`${input.pendingAuthorization.baseUrl}/oauth/token`, {
|
|
8827
|
+
method: 'POST',
|
|
8828
|
+
headers: {
|
|
8829
|
+
'content-type': 'application/json',
|
|
8830
|
+
},
|
|
8831
|
+
body: JSON.stringify({
|
|
8832
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
8833
|
+
device_code: input.pendingAuthorization.deviceCode,
|
|
8834
|
+
}),
|
|
8835
|
+
});
|
|
8836
|
+
const tokenPayload = await parseJsonResponse(tokenResponse);
|
|
8837
|
+
if (tokenResponse.ok && tokenPayload?.access_token) {
|
|
8838
|
+
const profiles = input.config.profiles ?? {};
|
|
8839
|
+
profiles[input.profileName] = storedProfileFromOAuthToken({
|
|
8840
|
+
baseUrl: input.pendingAuthorization.baseUrl,
|
|
8841
|
+
tokenPayload,
|
|
8842
|
+
shopifyAppUrl: input.pendingAuthorization.shopifyAppUrl ?? null,
|
|
8843
|
+
shopifyStore: input.pendingAuthorization.shopifyStore ?? null,
|
|
8844
|
+
});
|
|
8845
|
+
const updatedConfig = configWithoutPendingDeviceAuthorization({
|
|
8846
|
+
...input.config,
|
|
8847
|
+
defaultProfile: input.config.defaultProfile ?? input.profileName,
|
|
8848
|
+
profiles,
|
|
8849
|
+
}, input.profileName);
|
|
8850
|
+
await writeConfig(input.env, updatedConfig);
|
|
8851
|
+
const result = authLoginSuccessOutput({
|
|
8852
|
+
command: input.command,
|
|
8853
|
+
baseUrl: input.pendingAuthorization.baseUrl,
|
|
8854
|
+
tokenProfile: input.pendingAuthorization.tokenProfile,
|
|
8855
|
+
saveProfile: input.profileName,
|
|
8856
|
+
tokenPayload,
|
|
8857
|
+
shopifyAppUrl: input.pendingAuthorization.shopifyAppUrl ?? null,
|
|
8858
|
+
shopifyStore: input.pendingAuthorization.shopifyStore ?? null,
|
|
8859
|
+
configPath,
|
|
8860
|
+
});
|
|
8861
|
+
if (input.json) {
|
|
8862
|
+
input.io.stdout(JSON.stringify(result, null, 2));
|
|
8863
|
+
}
|
|
8864
|
+
else {
|
|
8865
|
+
input.io.stdout([
|
|
8866
|
+
'authenticated: true',
|
|
8867
|
+
`profile: ${input.profileName}`,
|
|
8868
|
+
`tenant_id: ${result.tenantId ?? '(unknown)'}`,
|
|
8869
|
+
`app_id: ${result.artifactId ?? '(account)'}`,
|
|
8870
|
+
`permissions: ${result.permissions.join(' ') || '(unknown)'}`,
|
|
8871
|
+
`expires_at: ${result.expiresAt ?? '(unknown)'}`,
|
|
8872
|
+
].join('\n'));
|
|
8873
|
+
}
|
|
8874
|
+
return 0;
|
|
8875
|
+
}
|
|
8876
|
+
if (tokenPayload?.error === 'authorization_pending' || tokenPayload?.error === 'slow_down') {
|
|
8877
|
+
const result = pendingDeviceAuthorizationOutput({
|
|
8878
|
+
command: input.command,
|
|
8879
|
+
baseUrl: input.pendingAuthorization.baseUrl,
|
|
8880
|
+
profileName: input.profileName,
|
|
8881
|
+
pendingAuthorization: input.pendingAuthorization,
|
|
8882
|
+
configPath,
|
|
8883
|
+
});
|
|
8884
|
+
if (input.json) {
|
|
8885
|
+
input.io.stdout(JSON.stringify(result, null, 2));
|
|
8886
|
+
}
|
|
8887
|
+
else {
|
|
8888
|
+
input.io.stdout([
|
|
8889
|
+
'authenticated: false',
|
|
8890
|
+
`profile: ${input.profileName}`,
|
|
8891
|
+
`verification_url: ${input.pendingAuthorization.verificationUriComplete}`,
|
|
8892
|
+
`user_code: ${input.pendingAuthorization.userCode}`,
|
|
8893
|
+
`next: ${result.nextCommand}`,
|
|
8894
|
+
].join('\n'));
|
|
8895
|
+
}
|
|
8896
|
+
return 1;
|
|
8897
|
+
}
|
|
8898
|
+
throw new Error(tokenPayload?.error_description ?? tokenPayload?.error ?? `Token exchange failed with HTTP ${tokenResponse.status}.`);
|
|
8899
|
+
}
|
|
8293
8900
|
async function runAuthStatus(command, io, runtime) {
|
|
8294
8901
|
const env = runtime.env ?? process.env;
|
|
8902
|
+
const fetcher = runtime.fetcher ?? fetch;
|
|
8903
|
+
const now = runtime.now ?? Date.now;
|
|
8295
8904
|
if (command.profileName) {
|
|
8296
8905
|
const config = await readConfig(env);
|
|
8906
|
+
const pendingAuthorization = config.pendingDeviceAuthorizations?.[command.profileName] ?? null;
|
|
8907
|
+
if (pendingAuthorization) {
|
|
8908
|
+
return await completePendingDeviceAuthorization({
|
|
8909
|
+
command: command.command,
|
|
8910
|
+
profileName: command.profileName,
|
|
8911
|
+
config,
|
|
8912
|
+
pendingAuthorization,
|
|
8913
|
+
env,
|
|
8914
|
+
fetcher,
|
|
8915
|
+
io,
|
|
8916
|
+
json: command.json,
|
|
8917
|
+
now,
|
|
8918
|
+
});
|
|
8919
|
+
}
|
|
8297
8920
|
const profile = config.profiles?.[command.profileName] ?? null;
|
|
8298
8921
|
const result = {
|
|
8299
8922
|
ok: Boolean(profile),
|
|
@@ -8355,6 +8978,20 @@ async function runAuthStatus(command, io, runtime) {
|
|
|
8355
8978
|
}
|
|
8356
8979
|
const config = await readConfig(env);
|
|
8357
8980
|
const profileName = command.profileName ?? config.defaultProfile ?? DEFAULT_PROFILE_NAME;
|
|
8981
|
+
const pendingAuthorization = config.pendingDeviceAuthorizations?.[profileName] ?? null;
|
|
8982
|
+
if (pendingAuthorization) {
|
|
8983
|
+
return await completePendingDeviceAuthorization({
|
|
8984
|
+
command: command.command,
|
|
8985
|
+
profileName,
|
|
8986
|
+
config,
|
|
8987
|
+
pendingAuthorization,
|
|
8988
|
+
env,
|
|
8989
|
+
fetcher,
|
|
8990
|
+
io,
|
|
8991
|
+
json: command.json,
|
|
8992
|
+
now,
|
|
8993
|
+
});
|
|
8994
|
+
}
|
|
8358
8995
|
const profile = config.profiles?.[profileName] ?? null;
|
|
8359
8996
|
const result = {
|
|
8360
8997
|
ok: Boolean(profile),
|
|
@@ -8419,7 +9056,11 @@ async function runAuthLogin(command, io, runtime) {
|
|
|
8419
9056
|
}),
|
|
8420
9057
|
});
|
|
8421
9058
|
const devicePayload = await parseJsonResponse(deviceResponse);
|
|
8422
|
-
if (!deviceResponse.ok ||
|
|
9059
|
+
if (!deviceResponse.ok ||
|
|
9060
|
+
!devicePayload?.device_code ||
|
|
9061
|
+
!devicePayload.user_code ||
|
|
9062
|
+
!devicePayload.verification_uri ||
|
|
9063
|
+
!devicePayload.verification_uri_complete) {
|
|
8423
9064
|
throw new Error(devicePayload?.error_description ?? devicePayload?.error ?? `Device authorization failed with HTTP ${deviceResponse.status}.`);
|
|
8424
9065
|
}
|
|
8425
9066
|
const verificationUriComplete = shopifyAppUrl
|
|
@@ -8429,19 +9070,51 @@ async function runAuthLogin(command, io, runtime) {
|
|
|
8429
9070
|
})
|
|
8430
9071
|
: devicePayload.verification_uri_complete;
|
|
8431
9072
|
const verificationUri = shopifyAppUrl ? shopifyDeviceVerificationUri(shopifyAppUrl) : devicePayload.verification_uri;
|
|
9073
|
+
const expiresAtMs = now() + Math.max(1, devicePayload.expires_in ?? 600) * 1000;
|
|
9074
|
+
const pendingAuthorization = {
|
|
9075
|
+
baseUrl,
|
|
9076
|
+
deviceCode: devicePayload.device_code,
|
|
9077
|
+
userCode: devicePayload.user_code,
|
|
9078
|
+
verificationUri,
|
|
9079
|
+
verificationUriComplete,
|
|
9080
|
+
expiresAt: expiresAtMs,
|
|
9081
|
+
interval: devicePayload.interval,
|
|
9082
|
+
tokenProfile: command.tokenProfile,
|
|
9083
|
+
artifactId: command.artifactId,
|
|
9084
|
+
ttl: command.ttl,
|
|
9085
|
+
...(shopifyAppUrl ? { shopifyAppUrl } : {}),
|
|
9086
|
+
...(shopifyStore ? { shopifyStore } : {}),
|
|
9087
|
+
};
|
|
9088
|
+
const pendingDeviceAuthorizations = {
|
|
9089
|
+
...(config.pendingDeviceAuthorizations ?? {}),
|
|
9090
|
+
[command.saveProfile]: pendingAuthorization,
|
|
9091
|
+
};
|
|
9092
|
+
const configWithPendingAuthorization = {
|
|
9093
|
+
...config,
|
|
9094
|
+
pendingDeviceAuthorizations: pendingDeviceAuthorizations,
|
|
9095
|
+
};
|
|
9096
|
+
await writeConfig(env, configWithPendingAuthorization);
|
|
8432
9097
|
const pendingResult = {
|
|
8433
|
-
ok:
|
|
9098
|
+
ok: false,
|
|
8434
9099
|
command: command.command,
|
|
9100
|
+
status: 'authorization_pending',
|
|
9101
|
+
authenticated: false,
|
|
9102
|
+
pending: true,
|
|
8435
9103
|
baseUrl,
|
|
8436
9104
|
shopifyAppUrl,
|
|
8437
9105
|
shopifyStore,
|
|
8438
9106
|
profile: command.tokenProfile,
|
|
8439
9107
|
saveProfile: command.saveProfile,
|
|
9108
|
+
pendingProfile: command.saveProfile,
|
|
9109
|
+
artifactId: command.artifactId,
|
|
8440
9110
|
userCode: devicePayload.user_code,
|
|
8441
9111
|
verificationUri,
|
|
8442
9112
|
verificationUriComplete,
|
|
8443
9113
|
expiresIn: devicePayload.expires_in ?? null,
|
|
9114
|
+
expiresAt: formatExpiry(expiresAtMs),
|
|
8444
9115
|
interval: devicePayload.interval ?? null,
|
|
9116
|
+
configPath: readConfigPath(env),
|
|
9117
|
+
nextCommand: buildAuthStatusNextCommand(command.saveProfile),
|
|
8445
9118
|
};
|
|
8446
9119
|
if (command.noPoll) {
|
|
8447
9120
|
if (command.json) {
|
|
@@ -8452,14 +9125,16 @@ async function runAuthLogin(command, io, runtime) {
|
|
|
8452
9125
|
}
|
|
8453
9126
|
return 0;
|
|
8454
9127
|
}
|
|
8455
|
-
if (
|
|
9128
|
+
if (command.json) {
|
|
9129
|
+
io.stderr(JSON.stringify(pendingResult, null, 2));
|
|
9130
|
+
}
|
|
9131
|
+
else {
|
|
8456
9132
|
io.stdout([
|
|
8457
9133
|
`Open this URL to approve Tender CLI access: ${verificationUriComplete}`,
|
|
8458
9134
|
`User code: ${devicePayload.user_code}`,
|
|
8459
9135
|
].join('\n'));
|
|
8460
9136
|
}
|
|
8461
9137
|
let intervalSeconds = Math.max(1, devicePayload.interval ?? 5);
|
|
8462
|
-
const expiresAtMs = now() + Math.max(1, devicePayload.expires_in ?? 600) * 1000;
|
|
8463
9138
|
let attemptsRemaining = Math.max(1, Math.ceil(command.pollTimeoutSeconds / intervalSeconds));
|
|
8464
9139
|
while (attemptsRemaining > 0 && now() < expiresAtMs) {
|
|
8465
9140
|
attemptsRemaining -= 1;
|
|
@@ -8475,38 +9150,29 @@ async function runAuthLogin(command, io, runtime) {
|
|
|
8475
9150
|
});
|
|
8476
9151
|
const tokenPayload = await parseJsonResponse(tokenResponse);
|
|
8477
9152
|
if (tokenResponse.ok && tokenPayload?.access_token) {
|
|
8478
|
-
const profiles =
|
|
8479
|
-
profiles[command.saveProfile] = {
|
|
9153
|
+
const profiles = configWithPendingAuthorization.profiles ?? {};
|
|
9154
|
+
profiles[command.saveProfile] = storedProfileFromOAuthToken({
|
|
8480
9155
|
baseUrl,
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
artifactId: tokenPayload.artifact_id,
|
|
8485
|
-
expiresAt: tokenPayload.expires_at,
|
|
8486
|
-
permissions: splitScope(tokenPayload.scope),
|
|
8487
|
-
...(shopifyAppUrl ? { shopifyAppUrl } : {}),
|
|
8488
|
-
...(shopifyStore ? { shopifyStore } : {}),
|
|
8489
|
-
};
|
|
8490
|
-
await writeConfig(env, {
|
|
8491
|
-
...config,
|
|
8492
|
-
defaultProfile: config.defaultProfile ?? command.saveProfile,
|
|
8493
|
-
profiles,
|
|
9156
|
+
tokenPayload,
|
|
9157
|
+
shopifyAppUrl,
|
|
9158
|
+
shopifyStore,
|
|
8494
9159
|
});
|
|
8495
|
-
const
|
|
8496
|
-
|
|
9160
|
+
const updatedConfig = configWithoutPendingDeviceAuthorization({
|
|
9161
|
+
...configWithPendingAuthorization,
|
|
9162
|
+
defaultProfile: configWithPendingAuthorization.defaultProfile ?? command.saveProfile,
|
|
9163
|
+
profiles,
|
|
9164
|
+
}, command.saveProfile);
|
|
9165
|
+
await writeConfig(env, updatedConfig);
|
|
9166
|
+
const result = authLoginSuccessOutput({
|
|
8497
9167
|
command: command.command,
|
|
8498
9168
|
baseUrl,
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
tenantId: tokenPayload.tenant_id ?? null,
|
|
8503
|
-
artifactId: tokenPayload.artifact_id ?? null,
|
|
8504
|
-
permissions: splitScope(tokenPayload.scope),
|
|
8505
|
-
expiresAt: formatExpiry(tokenPayload.expires_at),
|
|
9169
|
+
tokenProfile: command.tokenProfile,
|
|
9170
|
+
saveProfile: command.saveProfile,
|
|
9171
|
+
tokenPayload,
|
|
8506
9172
|
shopifyAppUrl,
|
|
8507
9173
|
shopifyStore,
|
|
8508
9174
|
configPath: readConfigPath(env),
|
|
8509
|
-
};
|
|
9175
|
+
});
|
|
8510
9176
|
if (command.json) {
|
|
8511
9177
|
io.stdout(JSON.stringify(result, null, 2));
|
|
8512
9178
|
}
|
|
@@ -9144,6 +9810,48 @@ async function runGitCommand(input) {
|
|
|
9144
9810
|
}
|
|
9145
9811
|
return result;
|
|
9146
9812
|
}
|
|
9813
|
+
async function resolveGitCommitArgument(input) {
|
|
9814
|
+
const value = input.value.trim();
|
|
9815
|
+
if (GIT_COMMIT_SHA_PATTERN.test(value)) {
|
|
9816
|
+
return value.toLowerCase();
|
|
9817
|
+
}
|
|
9818
|
+
if (!value || value.length > 256 || /[\0\r\n]/.test(value)) {
|
|
9819
|
+
throw new TenderCliUsageError('--commit must be a 40-character commit SHA or a Git ref resolvable in this checkout.');
|
|
9820
|
+
}
|
|
9821
|
+
const result = await runGitCommand({
|
|
9822
|
+
runtime: input.runtime,
|
|
9823
|
+
cwd: input.cwd,
|
|
9824
|
+
args: ['rev-parse', '--verify', `${value}^{commit}`],
|
|
9825
|
+
allowFailure: true,
|
|
9826
|
+
});
|
|
9827
|
+
const commitSha = result.stdout.trim().split(/\s+/)[0] ?? '';
|
|
9828
|
+
if (GIT_COMMIT_SHA_PATTERN.test(commitSha)) {
|
|
9829
|
+
return commitSha.toLowerCase();
|
|
9830
|
+
}
|
|
9831
|
+
const detail = result.stderr.trim() || result.stdout.trim();
|
|
9832
|
+
throw new TenderCliUsageError([
|
|
9833
|
+
`--commit must be a 40-character commit SHA or a Git ref resolvable in this checkout: ${value}`,
|
|
9834
|
+
detail,
|
|
9835
|
+
]
|
|
9836
|
+
.filter(Boolean)
|
|
9837
|
+
.join('\n'));
|
|
9838
|
+
}
|
|
9839
|
+
function readCommandCommitSha(command) {
|
|
9840
|
+
const commitSha = 'commitSha' in command ? command.commitSha : null;
|
|
9841
|
+
return typeof commitSha === 'string' && commitSha.trim() ? commitSha : null;
|
|
9842
|
+
}
|
|
9843
|
+
async function resolveCommandCommitSha(command, runtime) {
|
|
9844
|
+
const requestedCommitSha = readCommandCommitSha(command);
|
|
9845
|
+
if (!requestedCommitSha) {
|
|
9846
|
+
return command;
|
|
9847
|
+
}
|
|
9848
|
+
const commitSha = await resolveGitCommitArgument({
|
|
9849
|
+
runtime,
|
|
9850
|
+
cwd: process.cwd(),
|
|
9851
|
+
value: requestedCommitSha,
|
|
9852
|
+
});
|
|
9853
|
+
return commitSha === requestedCommitSha ? command : { ...command, commitSha };
|
|
9854
|
+
}
|
|
9147
9855
|
function shellQuote(value) {
|
|
9148
9856
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
9149
9857
|
}
|
|
@@ -10004,29 +10712,30 @@ async function runArtifactsInit(command, io, runtime) {
|
|
|
10004
10712
|
return 0;
|
|
10005
10713
|
}
|
|
10006
10714
|
async function postLifecycle(input) {
|
|
10007
|
-
const
|
|
10008
|
-
|
|
10009
|
-
|
|
10010
|
-
|
|
10715
|
+
const command = await resolveCommandCommitSha(input.command, input.runtime);
|
|
10716
|
+
const credentials = await resolveLifecycleApiCredentials({
|
|
10717
|
+
baseUrl: command.baseUrl,
|
|
10718
|
+
flagToken: command.flagToken,
|
|
10719
|
+
profileName: command.profileName,
|
|
10011
10720
|
env: input.runtime.env ?? process.env,
|
|
10012
10721
|
});
|
|
10013
10722
|
const fetcher = input.runtime.fetcher ?? fetch;
|
|
10014
|
-
const stream =
|
|
10723
|
+
const stream = command.stream;
|
|
10015
10724
|
const url = `${credentials.baseUrl}${input.path}${stream ? '?stream=1' : ''}`;
|
|
10016
10725
|
const requestBody = {};
|
|
10017
|
-
if ('commitSha' in
|
|
10018
|
-
requestBody.commitSha =
|
|
10726
|
+
if ('commitSha' in command && command.commitSha) {
|
|
10727
|
+
requestBody.commitSha = command.commitSha;
|
|
10019
10728
|
}
|
|
10020
|
-
if ('message' in
|
|
10021
|
-
requestBody.message =
|
|
10729
|
+
if ('message' in command && command.message) {
|
|
10730
|
+
requestBody.message = command.message;
|
|
10022
10731
|
}
|
|
10023
10732
|
const response = await fetcher(url, {
|
|
10024
10733
|
method: 'POST',
|
|
10025
|
-
headers: {
|
|
10026
|
-
|
|
10734
|
+
headers: apiHeadersForCredentials({
|
|
10735
|
+
credentials,
|
|
10027
10736
|
accept: stream ? 'text/event-stream' : 'application/json',
|
|
10028
|
-
|
|
10029
|
-
},
|
|
10737
|
+
jsonBody: Object.keys(requestBody).length > 0,
|
|
10738
|
+
}),
|
|
10030
10739
|
body: Object.keys(requestBody).length > 0 ? JSON.stringify(requestBody) : undefined,
|
|
10031
10740
|
});
|
|
10032
10741
|
if (stream) {
|
|
@@ -10036,19 +10745,19 @@ async function postLifecycle(input) {
|
|
|
10036
10745
|
await emitSseLifecycleStream({
|
|
10037
10746
|
response,
|
|
10038
10747
|
io: input.io,
|
|
10039
|
-
json:
|
|
10748
|
+
json: command.json,
|
|
10040
10749
|
});
|
|
10041
10750
|
await appendAutomaticMemory({
|
|
10042
10751
|
repoPath: process.cwd(),
|
|
10043
|
-
artifactId:
|
|
10752
|
+
artifactId: command.artifactId,
|
|
10044
10753
|
kind: input.operation === 'publish' ? 'change' : 'validation',
|
|
10045
|
-
summary: `${
|
|
10754
|
+
summary: `${command.command} streamed`,
|
|
10046
10755
|
body: `Operation: ${input.operation}\nStatus: streamed`,
|
|
10047
10756
|
tags: [input.operation, 'stream'],
|
|
10048
10757
|
status: 'resolved',
|
|
10049
10758
|
related: {
|
|
10050
|
-
commands: [
|
|
10051
|
-
...('commitSha' in
|
|
10759
|
+
commands: [command.command],
|
|
10760
|
+
...('commitSha' in command && command.commitSha ? { releaseId: command.commitSha } : {}),
|
|
10052
10761
|
},
|
|
10053
10762
|
runtime: input.runtime,
|
|
10054
10763
|
});
|
|
@@ -10066,12 +10775,12 @@ async function postLifecycle(input) {
|
|
|
10066
10775
|
}
|
|
10067
10776
|
const result = {
|
|
10068
10777
|
ok: true,
|
|
10069
|
-
command:
|
|
10778
|
+
command: command.command,
|
|
10070
10779
|
baseUrl: credentials.baseUrl,
|
|
10071
10780
|
authSource: credentials.source,
|
|
10072
10781
|
profile: credentials.profileName,
|
|
10073
|
-
artifactId:
|
|
10074
|
-
appId:
|
|
10782
|
+
artifactId: command.artifactId,
|
|
10783
|
+
appId: command.artifactId,
|
|
10075
10784
|
operation: payload.operation ?? input.operation,
|
|
10076
10785
|
job: payload.job ?? null,
|
|
10077
10786
|
result: payload.result ?? null,
|
|
@@ -10081,15 +10790,15 @@ async function postLifecycle(input) {
|
|
|
10081
10790
|
const releaseGitRef = input.operation === 'publish' ? readReleaseGitRefFromResult(result.result) : null;
|
|
10082
10791
|
const memory = await appendAutomaticMemory({
|
|
10083
10792
|
repoPath,
|
|
10084
|
-
artifactId:
|
|
10793
|
+
artifactId: command.artifactId,
|
|
10085
10794
|
kind: input.operation === 'validate' ? 'validation' : input.operation === 'publish' ? 'change' : 'validation',
|
|
10086
|
-
summary: `${
|
|
10795
|
+
summary: `${command.command} completed`,
|
|
10087
10796
|
body: `Operation: ${result.operation}\nStatus: completed`,
|
|
10088
10797
|
tags: [input.operation],
|
|
10089
10798
|
status: 'resolved',
|
|
10090
10799
|
related: {
|
|
10091
10800
|
files: changedFiles,
|
|
10092
|
-
commands: [
|
|
10801
|
+
commands: [command.command],
|
|
10093
10802
|
...(input.operation === 'preview' ? { previewUrl: readPreviewUrlFromJob(result.job) ?? undefined } : {}),
|
|
10094
10803
|
...(input.operation === 'publish' ? { publishedUrl: readObjectString(readObject(result.result), 'publishedUrl') ?? undefined } : {}),
|
|
10095
10804
|
...(releaseGitRef?.tagName ? { releaseId: releaseGitRef.tagName } : {}),
|
|
@@ -10104,12 +10813,12 @@ async function postLifecycle(input) {
|
|
|
10104
10813
|
memory,
|
|
10105
10814
|
...(memorySync ? { memorySync } : {}),
|
|
10106
10815
|
};
|
|
10107
|
-
if (
|
|
10816
|
+
if (command.json) {
|
|
10108
10817
|
input.io.stdout(JSON.stringify(resultWithMemory, null, 2));
|
|
10109
10818
|
}
|
|
10110
10819
|
else {
|
|
10111
10820
|
input.io.stdout([
|
|
10112
|
-
`app_id: ${
|
|
10821
|
+
`app_id: ${command.artifactId}`,
|
|
10113
10822
|
`operation: ${result.operation}`,
|
|
10114
10823
|
'status: completed',
|
|
10115
10824
|
...(input.operation === 'publish' ? formatPublishResultLines(result.result) : []),
|
|
@@ -10204,35 +10913,36 @@ async function requestPreviewStatus(input) {
|
|
|
10204
10913
|
};
|
|
10205
10914
|
}
|
|
10206
10915
|
async function runPreviewStatus(command, io, runtime) {
|
|
10916
|
+
const resolvedCommand = await resolveCommandCommitSha(command, runtime);
|
|
10207
10917
|
const credentials = await resolveApiCredentials({
|
|
10208
|
-
baseUrl:
|
|
10209
|
-
flagToken:
|
|
10210
|
-
profileName:
|
|
10918
|
+
baseUrl: resolvedCommand.baseUrl,
|
|
10919
|
+
flagToken: resolvedCommand.flagToken,
|
|
10920
|
+
profileName: resolvedCommand.profileName,
|
|
10211
10921
|
env: runtime.env ?? process.env,
|
|
10212
10922
|
});
|
|
10213
10923
|
const { job, preview } = await requestPreviewStatus({
|
|
10214
|
-
command,
|
|
10924
|
+
command: resolvedCommand,
|
|
10215
10925
|
credentials,
|
|
10216
10926
|
fetcher: runtime.fetcher ?? fetch,
|
|
10217
10927
|
});
|
|
10218
10928
|
const result = {
|
|
10219
10929
|
ok: true,
|
|
10220
|
-
command:
|
|
10930
|
+
command: resolvedCommand.command,
|
|
10221
10931
|
baseUrl: credentials.baseUrl,
|
|
10222
10932
|
authSource: credentials.source,
|
|
10223
10933
|
profile: credentials.profileName,
|
|
10224
|
-
artifactId:
|
|
10225
|
-
commitSha: preview.commitSha ??
|
|
10934
|
+
artifactId: resolvedCommand.artifactId,
|
|
10935
|
+
commitSha: preview.commitSha ?? resolvedCommand.commitSha,
|
|
10226
10936
|
status: preview.status ?? job.status ?? null,
|
|
10227
10937
|
previewUrl: preview.previewUrl ?? readPreviewUrlFromJob(job),
|
|
10228
10938
|
job,
|
|
10229
10939
|
};
|
|
10230
|
-
if (
|
|
10940
|
+
if (resolvedCommand.json) {
|
|
10231
10941
|
io.stdout(JSON.stringify(result, null, 2));
|
|
10232
10942
|
}
|
|
10233
10943
|
else {
|
|
10234
10944
|
io.stdout([
|
|
10235
|
-
`app_id: ${
|
|
10945
|
+
`app_id: ${resolvedCommand.artifactId}`,
|
|
10236
10946
|
`job_id: ${job.jobId}`,
|
|
10237
10947
|
`status: ${result.status ?? 'unknown'}`,
|
|
10238
10948
|
...(result.previewUrl ? [`preview_url: ${result.previewUrl}`] : []),
|
|
@@ -10241,19 +10951,20 @@ async function runPreviewStatus(command, io, runtime) {
|
|
|
10241
10951
|
return 0;
|
|
10242
10952
|
}
|
|
10243
10953
|
async function runPreviewWatch(command, io, runtime) {
|
|
10954
|
+
const resolvedCommand = await resolveCommandCommitSha(command, runtime);
|
|
10244
10955
|
const credentials = await resolveApiCredentials({
|
|
10245
|
-
baseUrl:
|
|
10246
|
-
flagToken:
|
|
10247
|
-
profileName:
|
|
10956
|
+
baseUrl: resolvedCommand.baseUrl,
|
|
10957
|
+
flagToken: resolvedCommand.flagToken,
|
|
10958
|
+
profileName: resolvedCommand.profileName,
|
|
10248
10959
|
env: runtime.env ?? process.env,
|
|
10249
10960
|
});
|
|
10250
10961
|
const { job } = await requestPreviewStatus({
|
|
10251
|
-
command,
|
|
10962
|
+
command: resolvedCommand,
|
|
10252
10963
|
credentials,
|
|
10253
10964
|
fetcher: runtime.fetcher ?? fetch,
|
|
10254
10965
|
});
|
|
10255
|
-
const stream =
|
|
10256
|
-
const response = await (runtime.fetcher ?? fetch)(`${credentials.baseUrl}/api/v1/artifacts/${encodeURIComponent(
|
|
10966
|
+
const stream = resolvedCommand.stream;
|
|
10967
|
+
const response = await (runtime.fetcher ?? fetch)(`${credentials.baseUrl}/api/v1/artifacts/${encodeURIComponent(resolvedCommand.artifactId)}/jobs/${encodeURIComponent(job.jobId)}/events${stream ? '?stream=1' : ''}`, {
|
|
10257
10968
|
headers: {
|
|
10258
10969
|
authorization: `Bearer ${credentials.token}`,
|
|
10259
10970
|
accept: stream ? 'text/event-stream' : 'application/json',
|
|
@@ -10266,7 +10977,7 @@ async function runPreviewWatch(command, io, runtime) {
|
|
|
10266
10977
|
await emitSseLifecycleStream({
|
|
10267
10978
|
response,
|
|
10268
10979
|
io,
|
|
10269
|
-
json:
|
|
10980
|
+
json: resolvedCommand.json,
|
|
10270
10981
|
});
|
|
10271
10982
|
return 0;
|
|
10272
10983
|
}
|
|
@@ -10276,15 +10987,15 @@ async function runPreviewWatch(command, io, runtime) {
|
|
|
10276
10987
|
}
|
|
10277
10988
|
const result = {
|
|
10278
10989
|
ok: true,
|
|
10279
|
-
command:
|
|
10990
|
+
command: resolvedCommand.command,
|
|
10280
10991
|
baseUrl: credentials.baseUrl,
|
|
10281
10992
|
authSource: credentials.source,
|
|
10282
10993
|
profile: credentials.profileName,
|
|
10283
|
-
artifactId:
|
|
10994
|
+
artifactId: resolvedCommand.artifactId,
|
|
10284
10995
|
job: payload.job ?? job,
|
|
10285
10996
|
events: payload.events ?? [],
|
|
10286
10997
|
};
|
|
10287
|
-
if (
|
|
10998
|
+
if (resolvedCommand.json) {
|
|
10288
10999
|
io.stdout(JSON.stringify(result, null, 2));
|
|
10289
11000
|
}
|
|
10290
11001
|
else {
|
|
@@ -10293,38 +11004,39 @@ async function runPreviewWatch(command, io, runtime) {
|
|
|
10293
11004
|
return 0;
|
|
10294
11005
|
}
|
|
10295
11006
|
async function runPublish(command, io, runtime) {
|
|
10296
|
-
|
|
11007
|
+
const resolvedCommand = await resolveCommandCommitSha(command, runtime);
|
|
11008
|
+
if (resolvedCommand.dryRun) {
|
|
10297
11009
|
const result = {
|
|
10298
11010
|
ok: true,
|
|
10299
|
-
command:
|
|
10300
|
-
artifactId:
|
|
10301
|
-
appId:
|
|
10302
|
-
commitSha:
|
|
11011
|
+
command: resolvedCommand.command,
|
|
11012
|
+
artifactId: resolvedCommand.artifactId,
|
|
11013
|
+
appId: resolvedCommand.artifactId,
|
|
11014
|
+
commitSha: resolvedCommand.commitSha,
|
|
10303
11015
|
dryRun: true,
|
|
10304
11016
|
confirmed: false,
|
|
10305
|
-
message:
|
|
11017
|
+
message: resolvedCommand.message,
|
|
10306
11018
|
wouldRequest: {
|
|
10307
11019
|
method: 'POST',
|
|
10308
|
-
path: `/api/v1/artifacts/${
|
|
10309
|
-
stream:
|
|
11020
|
+
path: `/api/v1/artifacts/${resolvedCommand.artifactId}/publish`,
|
|
11021
|
+
stream: resolvedCommand.stream,
|
|
10310
11022
|
body: {
|
|
10311
|
-
...(
|
|
10312
|
-
...(
|
|
11023
|
+
...(resolvedCommand.commitSha ? { commitSha: resolvedCommand.commitSha } : {}),
|
|
11024
|
+
...(resolvedCommand.message ? { message: resolvedCommand.message } : {}),
|
|
10313
11025
|
},
|
|
10314
11026
|
},
|
|
10315
11027
|
};
|
|
10316
|
-
if (
|
|
11028
|
+
if (resolvedCommand.json) {
|
|
10317
11029
|
io.stdout(JSON.stringify(result, null, 2));
|
|
10318
11030
|
}
|
|
10319
11031
|
else {
|
|
10320
|
-
io.stdout([`app_id: ${
|
|
11032
|
+
io.stdout([`app_id: ${resolvedCommand.artifactId}`, 'dry_run: true', 'next: rerun with --confirm publish to publish.'].join('\n'));
|
|
10321
11033
|
}
|
|
10322
11034
|
return 0;
|
|
10323
11035
|
}
|
|
10324
11036
|
return await postLifecycle({
|
|
10325
|
-
command,
|
|
11037
|
+
command: resolvedCommand,
|
|
10326
11038
|
operation: 'publish',
|
|
10327
|
-
path: `/api/v1/artifacts/${encodeURIComponent(
|
|
11039
|
+
path: `/api/v1/artifacts/${encodeURIComponent(resolvedCommand.artifactId)}/publish`,
|
|
10328
11040
|
io,
|
|
10329
11041
|
runtime,
|
|
10330
11042
|
});
|
|
@@ -10994,7 +11706,7 @@ async function resolvePlaybooksCredentials(command, runtime) {
|
|
|
10994
11706
|
if (error instanceof TenderCliUsageError) {
|
|
10995
11707
|
throw new TenderCliUsageError([
|
|
10996
11708
|
'Playbooks require Tender authentication.',
|
|
10997
|
-
'Run `tender auth login --device --profile edit-preview --save-profile agent --ttl 7d --json`, set TENDER_API_TOKEN
|
|
11709
|
+
'Run `tender auth login --device --profile edit-preview --save-profile agent --ttl 7d --json`, approve the URL, then run `tender auth status --profile agent --json`; or set TENDER_API_TOKEN or pass --token <token>.',
|
|
10998
11710
|
].join('\n'));
|
|
10999
11711
|
}
|
|
11000
11712
|
throw error;
|
|
@@ -11098,7 +11810,7 @@ async function resolveConnectorsCredentials(command, runtime) {
|
|
|
11098
11810
|
if (error instanceof TenderCliUsageError) {
|
|
11099
11811
|
throw new TenderCliUsageError([
|
|
11100
11812
|
'Connectors require Tender account authentication.',
|
|
11101
|
-
'Run `tender auth login --device --profile create --ttl 7d --save-profile account --json`, set TENDER_API_TOKEN
|
|
11813
|
+
'Run `tender auth login --device --profile create --ttl 7d --save-profile account --json`, approve the URL, then run `tender auth status --profile account --json`; or set TENDER_API_TOKEN or pass --token <token>.',
|
|
11102
11814
|
].join('\n'));
|
|
11103
11815
|
}
|
|
11104
11816
|
throw error;
|
|
@@ -11123,6 +11835,7 @@ async function runConnectorsList(command, io, runtime) {
|
|
|
11123
11835
|
baseUrl: credentials.baseUrl,
|
|
11124
11836
|
token: credentials.token,
|
|
11125
11837
|
fetcher: runtime.fetcher ?? fetch,
|
|
11838
|
+
hostedWorkspaceHint: Boolean((runtime.env ?? process.env).TENDER_RUNTIME_ARTIFACT_ID),
|
|
11126
11839
|
});
|
|
11127
11840
|
const connectors = readConnectorArray(payload);
|
|
11128
11841
|
printConnectorsPayload(command, io, payload, connectors.length > 0
|
|
@@ -11418,6 +12131,7 @@ merchant-editable settings.
|
|
|
11418
12131
|
['extensions/tender-product-survey/package.json', '{\n "name": "tender-product-survey",\n "private": true,\n "version": "0.0.0"\n}\n'],
|
|
11419
12132
|
['extensions/tender-product-survey/shopify.extension.toml', extensionToml],
|
|
11420
12133
|
['extensions/tender-product-survey/blocks/product_survey.liquid', blockLiquid],
|
|
12134
|
+
['extensions/tender-product-survey/locales/en.default.json', '{}\n'],
|
|
11421
12135
|
]);
|
|
11422
12136
|
}
|
|
11423
12137
|
function buildShopifySourceFiles(input) {
|
|
@@ -11514,6 +12228,17 @@ npx --yes @shopify/cli@latest app config validate --path . --json
|
|
|
11514
12228
|
\`\`\`sh
|
|
11515
12229
|
tender connectors shopify source deploy ${input.connectorId} --dir . --confirm deploy --profile account --json
|
|
11516
12230
|
\`\`\`
|
|
12231
|
+
|
|
12232
|
+
Inside the hosted Shopify workspace, commit and push source changes, then
|
|
12233
|
+
validate or deploy from the embedded Shopify app's Deploy source action so
|
|
12234
|
+
Tender can use the connector's stored App Automation Token server-side. If
|
|
12235
|
+
Shopify CLI is already authenticated in the hosted workspace, direct
|
|
12236
|
+
\`shopify app config validate\` and \`shopify app deploy\` are allowed. Do not run
|
|
12237
|
+
\`tender connectors shopify source validate\` or
|
|
12238
|
+
\`tender connectors shopify source deploy\` from hosted workspaces; those
|
|
12239
|
+
commands require an account-scoped Tender CLI token. Never ask for Shopify App
|
|
12240
|
+
Automation Tokens, client secrets, Tender tokens, or provider keys in the
|
|
12241
|
+
terminal.
|
|
11517
12242
|
`;
|
|
11518
12243
|
const files = new Map([
|
|
11519
12244
|
['.gitignore', '.shopify/\nnode_modules/\n.env\n.env.*\n'],
|
|
@@ -12058,6 +12783,264 @@ async function runConnectorsShopifySourceDeploy(command, io, runtime) {
|
|
|
12058
12783
|
printConnectorsPayload(command, io, { ok: true, command: command.command, connectorId: command.connectorId, sourceArtifactId, dir, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr }, [`connector_id: ${command.connectorId}`, `source_artifact_id: ${sourceArtifactId}`, `source_dir: ${dir}`, 'deploy: completed']);
|
|
12059
12784
|
return 0;
|
|
12060
12785
|
}
|
|
12786
|
+
function shopifyWorkspaceResultKey(command) {
|
|
12787
|
+
return command === 'shopify workspace deploy-source' ? 'deployment' : 'validation';
|
|
12788
|
+
}
|
|
12789
|
+
function shopifyWorkspaceCommandSucceeded(command, payload) {
|
|
12790
|
+
if (payload.ok !== true) {
|
|
12791
|
+
return false;
|
|
12792
|
+
}
|
|
12793
|
+
const result = payload[shopifyWorkspaceResultKey(command.command)];
|
|
12794
|
+
if (result && typeof result === 'object' && !Array.isArray(result)) {
|
|
12795
|
+
const ok = result.ok;
|
|
12796
|
+
return ok !== false;
|
|
12797
|
+
}
|
|
12798
|
+
return true;
|
|
12799
|
+
}
|
|
12800
|
+
function shopifyWorkspaceTextLines(command, payload) {
|
|
12801
|
+
const connector = payload.connector && typeof payload.connector === 'object' && !Array.isArray(payload.connector)
|
|
12802
|
+
? payload.connector
|
|
12803
|
+
: {};
|
|
12804
|
+
const source = payload.source && typeof payload.source === 'object' && !Array.isArray(payload.source)
|
|
12805
|
+
? payload.source
|
|
12806
|
+
: {};
|
|
12807
|
+
const result = payload[shopifyWorkspaceResultKey(command.command)];
|
|
12808
|
+
const resultRecord = result && typeof result === 'object' && !Array.isArray(result)
|
|
12809
|
+
? result
|
|
12810
|
+
: {};
|
|
12811
|
+
const succeeded = shopifyWorkspaceCommandSucceeded(command, payload);
|
|
12812
|
+
const verb = command.command === 'shopify workspace deploy-source' ? 'deploy' : 'validate';
|
|
12813
|
+
return [
|
|
12814
|
+
`${verb}: ${succeeded ? 'completed' : 'failed'}`,
|
|
12815
|
+
`connector_id: ${String(connector.connectorId ?? '')}`,
|
|
12816
|
+
`source_artifact_id: ${String(source.sourceArtifactId ?? '')}`,
|
|
12817
|
+
...(payload.reasonCode ? [`reason: ${String(payload.reasonCode)}`] : []),
|
|
12818
|
+
...(resultRecord.exitCode !== undefined ? [`exit_code: ${String(resultRecord.exitCode)}`] : []),
|
|
12819
|
+
...(resultRecord.stderr ? [`stderr: ${String(resultRecord.stderr).trim()}`] : []),
|
|
12820
|
+
].filter((line) => !line.endsWith(': '));
|
|
12821
|
+
}
|
|
12822
|
+
function readShopifyWorkspaceRuntimeIdentity(runtime) {
|
|
12823
|
+
const env = runtime.env ?? process.env;
|
|
12824
|
+
const baseUrl = normalizeHostedWorkspaceBaseUrl(env.TENDER_BASE_URL);
|
|
12825
|
+
if (!baseUrl) {
|
|
12826
|
+
throw new TenderCliUsageError('TENDER_BASE_URL is required. Open or reload the hosted Shopify workspace and run this command inside its terminal.');
|
|
12827
|
+
}
|
|
12828
|
+
if (!isHostedWorkspaceInternalApiBaseUrl(baseUrl)) {
|
|
12829
|
+
throw new TenderCliUsageError('TENDER_BASE_URL must be http://tender-api.tender.test inside a hosted Shopify workspace. Reload the workspace terminal and try again.');
|
|
12830
|
+
}
|
|
12831
|
+
const runtimeArtifactId = readHostedWorkspaceArtifactId(env.TENDER_RUNTIME_ARTIFACT_ID);
|
|
12832
|
+
if (!runtimeArtifactId) {
|
|
12833
|
+
throw new TenderCliUsageError('TENDER_RUNTIME_ARTIFACT_ID is required. This command only runs inside a hosted Shopify project workspace.');
|
|
12834
|
+
}
|
|
12835
|
+
return { baseUrl, runtimeArtifactId };
|
|
12836
|
+
}
|
|
12837
|
+
function readShopifyWorkspaceConnectorRecords(payload) {
|
|
12838
|
+
return Array.isArray(payload.connectors)
|
|
12839
|
+
? payload.connectors.filter((connector) => Boolean(connector) && typeof connector === 'object' && !Array.isArray(connector))
|
|
12840
|
+
: [];
|
|
12841
|
+
}
|
|
12842
|
+
function readShopifyWorkspaceConnectorSourceArtifactId(connector) {
|
|
12843
|
+
const sourceProject = connector.sourceProject && typeof connector.sourceProject === 'object' && !Array.isArray(connector.sourceProject)
|
|
12844
|
+
? connector.sourceProject
|
|
12845
|
+
: null;
|
|
12846
|
+
return readHostedWorkspaceArtifactId(typeof sourceProject?.sourceArtifactId === 'string' ? sourceProject.sourceArtifactId : undefined);
|
|
12847
|
+
}
|
|
12848
|
+
function resolveShopifyWorkspaceSourceCommandConnector(input) {
|
|
12849
|
+
const connectors = readShopifyWorkspaceConnectorRecords(input.payload);
|
|
12850
|
+
const connector = input.selectedConnectorId
|
|
12851
|
+
? connectors.find((candidate) => candidate.connectorId === input.selectedConnectorId) ?? null
|
|
12852
|
+
: (() => {
|
|
12853
|
+
const linked = connectors.filter((candidate) => candidate.linked === true);
|
|
12854
|
+
return linked.length === 1 ? linked[0] ?? null : null;
|
|
12855
|
+
})();
|
|
12856
|
+
if (!connector) {
|
|
12857
|
+
return {
|
|
12858
|
+
ok: false,
|
|
12859
|
+
payload: {
|
|
12860
|
+
ok: false,
|
|
12861
|
+
reasonCode: input.selectedConnectorId
|
|
12862
|
+
? 'shopify_workspace_selected_connector_not_found'
|
|
12863
|
+
: 'shopify_workspace_connector_selection_required',
|
|
12864
|
+
message: input.selectedConnectorId
|
|
12865
|
+
? 'The selected Shopify connector is no longer available to this workspace.'
|
|
12866
|
+
: 'Link this runtime to one Shopify connector before validating or deploying source.',
|
|
12867
|
+
},
|
|
12868
|
+
};
|
|
12869
|
+
}
|
|
12870
|
+
const connectorId = typeof connector.connectorId === 'string' ? connector.connectorId : null;
|
|
12871
|
+
const sourceArtifactId = readShopifyWorkspaceConnectorSourceArtifactId(connector);
|
|
12872
|
+
if (!connectorId || !sourceArtifactId) {
|
|
12873
|
+
return {
|
|
12874
|
+
ok: false,
|
|
12875
|
+
payload: {
|
|
12876
|
+
ok: false,
|
|
12877
|
+
reasonCode: 'shopify_source_project_required',
|
|
12878
|
+
message: 'The selected Shopify connector does not have a source project.',
|
|
12879
|
+
},
|
|
12880
|
+
};
|
|
12881
|
+
}
|
|
12882
|
+
return {
|
|
12883
|
+
ok: true,
|
|
12884
|
+
connectorId,
|
|
12885
|
+
sourceArtifactId,
|
|
12886
|
+
};
|
|
12887
|
+
}
|
|
12888
|
+
async function runShopifyWorkspaceSourceCommand(command, io, runtime) {
|
|
12889
|
+
const { baseUrl, runtimeArtifactId } = readShopifyWorkspaceRuntimeIdentity(runtime);
|
|
12890
|
+
const selectedConnectorId = await readHostedWorkspaceConnectorSelection(runtimeArtifactId, runtime);
|
|
12891
|
+
const connectorsResult = await requestShopifyWorkspaceConnectorsList({
|
|
12892
|
+
baseUrl,
|
|
12893
|
+
runtimeArtifactId,
|
|
12894
|
+
fetcher: runtime.fetcher ?? fetch,
|
|
12895
|
+
});
|
|
12896
|
+
if (!shopifyWorkspaceRequestSucceeded(connectorsResult)) {
|
|
12897
|
+
const payload = {
|
|
12898
|
+
...connectorsResult.payload,
|
|
12899
|
+
command: command.command,
|
|
12900
|
+
runtimeArtifactId,
|
|
12901
|
+
...(selectedConnectorId ? { selectedConnectorId } : {}),
|
|
12902
|
+
};
|
|
12903
|
+
if (command.json) {
|
|
12904
|
+
io.stdout(JSON.stringify(payload, null, 2));
|
|
12905
|
+
}
|
|
12906
|
+
else {
|
|
12907
|
+
io.stdout(shopifyWorkspaceTextLines(command, payload).join('\n'));
|
|
12908
|
+
}
|
|
12909
|
+
return 1;
|
|
12910
|
+
}
|
|
12911
|
+
const resolvedConnector = resolveShopifyWorkspaceSourceCommandConnector({
|
|
12912
|
+
payload: connectorsResult.payload,
|
|
12913
|
+
selectedConnectorId,
|
|
12914
|
+
});
|
|
12915
|
+
if (!resolvedConnector.ok) {
|
|
12916
|
+
const payload = {
|
|
12917
|
+
...resolvedConnector.payload,
|
|
12918
|
+
command: command.command,
|
|
12919
|
+
runtimeArtifactId,
|
|
12920
|
+
...(selectedConnectorId ? { selectedConnectorId } : {}),
|
|
12921
|
+
};
|
|
12922
|
+
if (command.json) {
|
|
12923
|
+
io.stdout(JSON.stringify(payload, null, 2));
|
|
12924
|
+
}
|
|
12925
|
+
else {
|
|
12926
|
+
io.stdout(shopifyWorkspaceTextLines(command, payload).join('\n'));
|
|
12927
|
+
}
|
|
12928
|
+
return 1;
|
|
12929
|
+
}
|
|
12930
|
+
const result = await requestShopifyWorkspaceSourceCommand({
|
|
12931
|
+
command: command.command,
|
|
12932
|
+
baseUrl,
|
|
12933
|
+
runtimeArtifactId,
|
|
12934
|
+
fetcher: runtime.fetcher ?? fetch,
|
|
12935
|
+
connectorId: resolvedConnector.connectorId,
|
|
12936
|
+
sourceArtifactId: resolvedConnector.sourceArtifactId,
|
|
12937
|
+
});
|
|
12938
|
+
const payload = {
|
|
12939
|
+
...result.payload,
|
|
12940
|
+
command: command.command,
|
|
12941
|
+
runtimeArtifactId,
|
|
12942
|
+
selectedConnectorId: resolvedConnector.connectorId,
|
|
12943
|
+
};
|
|
12944
|
+
if (command.json) {
|
|
12945
|
+
io.stdout(JSON.stringify(payload, null, 2));
|
|
12946
|
+
}
|
|
12947
|
+
else {
|
|
12948
|
+
io.stdout(shopifyWorkspaceTextLines(command, payload).join('\n'));
|
|
12949
|
+
}
|
|
12950
|
+
return shopifyWorkspaceCommandSucceeded(command, payload) ? 0 : 1;
|
|
12951
|
+
}
|
|
12952
|
+
function shopifyWorkspaceRequestSucceeded(result) {
|
|
12953
|
+
return result.status >= 200 && result.status < 300 && result.payload.ok !== false;
|
|
12954
|
+
}
|
|
12955
|
+
function shopifyWorkspaceConnectorListTextLines(payload) {
|
|
12956
|
+
const connectors = readShopifyWorkspaceConnectorRecords(payload);
|
|
12957
|
+
const lines = [
|
|
12958
|
+
`runtime_artifact_id: ${String(payload.runtimeArtifactId ?? '')}`,
|
|
12959
|
+
`shop_domain: ${String(payload.shopDomain ?? '')}`,
|
|
12960
|
+
`connectors: ${connectors.length}`,
|
|
12961
|
+
];
|
|
12962
|
+
for (const connector of connectors) {
|
|
12963
|
+
lines.push([
|
|
12964
|
+
String(connector.connectorId ?? ''),
|
|
12965
|
+
String(connector.label ?? connector.appName ?? ''),
|
|
12966
|
+
connector.linked ? 'linked' : 'not_linked',
|
|
12967
|
+
connector.ready ? 'ready' : 'needs_setup',
|
|
12968
|
+
]
|
|
12969
|
+
.filter(Boolean)
|
|
12970
|
+
.join(' | '));
|
|
12971
|
+
}
|
|
12972
|
+
if (typeof payload.reasonCode === 'string') {
|
|
12973
|
+
lines.push(`reason: ${payload.reasonCode}`);
|
|
12974
|
+
}
|
|
12975
|
+
return lines.filter((line) => !line.endsWith(': '));
|
|
12976
|
+
}
|
|
12977
|
+
function shopifyWorkspaceLinkRuntimeTextLines(payload) {
|
|
12978
|
+
const connector = payload.connector && typeof payload.connector === 'object' && !Array.isArray(payload.connector)
|
|
12979
|
+
? payload.connector
|
|
12980
|
+
: {};
|
|
12981
|
+
const source = payload.source && typeof payload.source === 'object' && !Array.isArray(payload.source)
|
|
12982
|
+
? payload.source
|
|
12983
|
+
: {};
|
|
12984
|
+
const workspace = payload.workspace && typeof payload.workspace === 'object' && !Array.isArray(payload.workspace)
|
|
12985
|
+
? payload.workspace
|
|
12986
|
+
: {};
|
|
12987
|
+
return [
|
|
12988
|
+
`link: ${payload.ok === false ? 'failed' : 'created'}`,
|
|
12989
|
+
`runtime_artifact_id: ${String(payload.runtimeArtifactId ?? '')}`,
|
|
12990
|
+
`connector_id: ${String(connector.connectorId ?? '')}`,
|
|
12991
|
+
`source_artifact_id: ${String(source.sourceArtifactId ?? '')}`,
|
|
12992
|
+
workspace.reloadRequired ? 'next: reload the hosted workspace to mount /workspace/shopify-connector' : '',
|
|
12993
|
+
...(payload.reasonCode ? [`reason: ${String(payload.reasonCode)}`] : []),
|
|
12994
|
+
].filter(Boolean);
|
|
12995
|
+
}
|
|
12996
|
+
async function runShopifyWorkspaceConnectorsList(command, io, runtime) {
|
|
12997
|
+
const { baseUrl, runtimeArtifactId } = readShopifyWorkspaceRuntimeIdentity(runtime);
|
|
12998
|
+
const result = await requestShopifyWorkspaceConnectorsList({
|
|
12999
|
+
baseUrl,
|
|
13000
|
+
runtimeArtifactId,
|
|
13001
|
+
fetcher: runtime.fetcher ?? fetch,
|
|
13002
|
+
});
|
|
13003
|
+
const payload = {
|
|
13004
|
+
...result.payload,
|
|
13005
|
+
command: command.command,
|
|
13006
|
+
runtimeArtifactId,
|
|
13007
|
+
};
|
|
13008
|
+
if (command.json) {
|
|
13009
|
+
io.stdout(JSON.stringify(payload, null, 2));
|
|
13010
|
+
}
|
|
13011
|
+
else {
|
|
13012
|
+
io.stdout(shopifyWorkspaceConnectorListTextLines(payload).join('\n'));
|
|
13013
|
+
}
|
|
13014
|
+
return shopifyWorkspaceRequestSucceeded(result) ? 0 : 1;
|
|
13015
|
+
}
|
|
13016
|
+
async function runShopifyWorkspaceLinkRuntime(command, io, runtime) {
|
|
13017
|
+
const { baseUrl, runtimeArtifactId } = readShopifyWorkspaceRuntimeIdentity(runtime);
|
|
13018
|
+
const result = await requestShopifyWorkspaceLinkRuntime({
|
|
13019
|
+
baseUrl,
|
|
13020
|
+
runtimeArtifactId,
|
|
13021
|
+
connectorId: command.connectorId,
|
|
13022
|
+
fetcher: runtime.fetcher ?? fetch,
|
|
13023
|
+
});
|
|
13024
|
+
if (shopifyWorkspaceRequestSucceeded(result)) {
|
|
13025
|
+
await writeHostedWorkspaceConnectorSelection({
|
|
13026
|
+
runtimeArtifactId,
|
|
13027
|
+
connectorId: command.connectorId,
|
|
13028
|
+
}, runtime);
|
|
13029
|
+
}
|
|
13030
|
+
const payload = {
|
|
13031
|
+
...result.payload,
|
|
13032
|
+
command: command.command,
|
|
13033
|
+
runtimeArtifactId,
|
|
13034
|
+
connectorId: command.connectorId,
|
|
13035
|
+
};
|
|
13036
|
+
if (command.json) {
|
|
13037
|
+
io.stdout(JSON.stringify(payload, null, 2));
|
|
13038
|
+
}
|
|
13039
|
+
else {
|
|
13040
|
+
io.stdout(shopifyWorkspaceLinkRuntimeTextLines(payload).join('\n'));
|
|
13041
|
+
}
|
|
13042
|
+
return shopifyWorkspaceRequestSucceeded(result) ? 0 : 1;
|
|
13043
|
+
}
|
|
12061
13044
|
async function runConnectorsShopifyDelete(command, io, runtime) {
|
|
12062
13045
|
if (command.confirm !== 'delete') {
|
|
12063
13046
|
throw new TenderCliUsageError('--confirm delete is required. Example: tender connectors shopify delete conn_123 --confirm delete --json');
|
|
@@ -12966,6 +13949,9 @@ function exampleForError(error, args, command) {
|
|
|
12966
13949
|
if (error instanceof TenderCliDbApiError || error instanceof TenderCliAnalyticsApiError) {
|
|
12967
13950
|
return error.example;
|
|
12968
13951
|
}
|
|
13952
|
+
if (error instanceof TenderCliConnectorsApiError) {
|
|
13953
|
+
return error.example;
|
|
13954
|
+
}
|
|
12969
13955
|
const commandName = commandNameForError(args, command);
|
|
12970
13956
|
const dbPath = dbPathFromCommandName(commandName);
|
|
12971
13957
|
const artifactId = artifactIdForError(args, command) ?? 'artifact_123';
|
|
@@ -13092,6 +14078,14 @@ export async function runTenderCli(args, io = {
|
|
|
13092
14078
|
else if (command.topic === 'connectors bind') {
|
|
13093
14079
|
io.stdout(connectorsBindHelp());
|
|
13094
14080
|
}
|
|
14081
|
+
else if (command.topic === 'shopify' ||
|
|
14082
|
+
command.topic === 'shopify workspace' ||
|
|
14083
|
+
command.topic === 'shopify workspace connectors list' ||
|
|
14084
|
+
command.topic === 'shopify workspace link-runtime' ||
|
|
14085
|
+
command.topic === 'shopify workspace validate-source' ||
|
|
14086
|
+
command.topic === 'shopify workspace deploy-source') {
|
|
14087
|
+
io.stdout(shopifyWorkspaceHelp());
|
|
14088
|
+
}
|
|
13095
14089
|
else if (command.topic === 'auth') {
|
|
13096
14090
|
io.stdout(authHelp());
|
|
13097
14091
|
}
|
|
@@ -13352,6 +14346,15 @@ export async function runTenderCli(args, io = {
|
|
|
13352
14346
|
if (command.command === 'connectors shopify source deploy') {
|
|
13353
14347
|
return await runConnectorsShopifySourceDeploy(command, io, runtime);
|
|
13354
14348
|
}
|
|
14349
|
+
if (command.command === 'shopify workspace validate-source' || command.command === 'shopify workspace deploy-source') {
|
|
14350
|
+
return await runShopifyWorkspaceSourceCommand(command, io, runtime);
|
|
14351
|
+
}
|
|
14352
|
+
if (command.command === 'shopify workspace connectors list') {
|
|
14353
|
+
return await runShopifyWorkspaceConnectorsList(command, io, runtime);
|
|
14354
|
+
}
|
|
14355
|
+
if (command.command === 'shopify workspace link-runtime') {
|
|
14356
|
+
return await runShopifyWorkspaceLinkRuntime(command, io, runtime);
|
|
14357
|
+
}
|
|
13355
14358
|
if (command.command === 'connectors shopify delete') {
|
|
13356
14359
|
return await runConnectorsShopifyDelete(command, io, runtime);
|
|
13357
14360
|
}
|
|
@@ -13524,7 +14527,7 @@ export async function runTenderCli(args, io = {
|
|
|
13524
14527
|
}
|
|
13525
14528
|
catch (error) {
|
|
13526
14529
|
if (args.includes('--json') &&
|
|
13527
|
-
((args[0] === 'app' && (args[1] === 'db' || (args[1] === 'agent' && args[2] === 'heartbeat'))) || args[0] === 'playbooks' || args[0] === 'connectors')) {
|
|
14530
|
+
((args[0] === 'app' && (args[1] === 'db' || (args[1] === 'agent' && args[2] === 'heartbeat'))) || args[0] === 'playbooks' || args[0] === 'connectors' || args[0] === 'shopify')) {
|
|
13528
14531
|
io.stdout(JSON.stringify(cliErrorPayload(error, args, command), null, 2));
|
|
13529
14532
|
return error instanceof TenderCliUsageError ? 2 : 1;
|
|
13530
14533
|
}
|