@tenderprompt/cli 0.1.32 → 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 +28 -5
- package/dist/index.js +1327 -162
- 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,6 +90,7 @@ const ANALYTICS_AGENT_GUIDANCE = {
|
|
|
89
90
|
'clear split between shopper-facing UI and operator-facing analytics',
|
|
90
91
|
],
|
|
91
92
|
};
|
|
93
|
+
const DEFAULT_TENDER_APP_SCAFFOLD_TEMPLATE = 'server-backed';
|
|
92
94
|
class TenderCliUsageError extends Error {
|
|
93
95
|
constructor(message) {
|
|
94
96
|
super(message);
|
|
@@ -133,12 +135,14 @@ class TenderCliConnectorsApiError extends Error {
|
|
|
133
135
|
reasonCode;
|
|
134
136
|
status;
|
|
135
137
|
payload;
|
|
136
|
-
|
|
138
|
+
example;
|
|
139
|
+
constructor(message, reasonCode, status, payload = null, example = 'tender connectors list --profile account --json') {
|
|
137
140
|
super(message);
|
|
138
141
|
this.name = 'TenderCliConnectorsApiError';
|
|
139
142
|
this.reasonCode = reasonCode;
|
|
140
143
|
this.status = status;
|
|
141
144
|
this.payload = payload;
|
|
145
|
+
this.example = example;
|
|
142
146
|
}
|
|
143
147
|
}
|
|
144
148
|
const DEFAULT_PROFILE_NAME = 'default';
|
|
@@ -176,6 +180,12 @@ Commands:
|
|
|
176
180
|
tender connectors shopify verify
|
|
177
181
|
Exchange credentials for an Admin API access token
|
|
178
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
|
|
179
189
|
tender app list List Tender App records visible to the current token
|
|
180
190
|
tender app create --name <name>
|
|
181
191
|
Create a new Tender App in the current account
|
|
@@ -238,8 +248,8 @@ Commands:
|
|
|
238
248
|
Examples:
|
|
239
249
|
tender --version
|
|
240
250
|
tender capabilities --json
|
|
241
|
-
tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d
|
|
242
|
-
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
|
|
243
253
|
tender memory add --kind decision --summary "Kept auth in the host gateway" --json
|
|
244
254
|
tender memory handoff --reason "Preview publish is blocked" --summary "Need Tender support" --json
|
|
245
255
|
tender memory brief --json
|
|
@@ -253,13 +263,18 @@ Examples:
|
|
|
253
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
|
|
254
264
|
tender connectors shopify verify conn_123 --profile account --json
|
|
255
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
|
|
256
269
|
tender app list --json
|
|
257
270
|
tender app create --name "Exit Intent Widget" --json
|
|
258
|
-
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
|
|
259
273
|
tender app update artifact_123 --name "Sale Widget" --json
|
|
260
274
|
tender app share create artifact_123 --expires 7d --claims 1 --json
|
|
261
275
|
tender app claim tok_share_123 --dir ./widget --confirm source-and-setup --json
|
|
262
|
-
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
|
|
263
278
|
tender app validate artifact_123 --json
|
|
264
279
|
tender app build artifact_123 --commit $(git rev-parse HEAD) --json
|
|
265
280
|
tender app preview artifact_123 --commit $(git rev-parse HEAD) --stream --json
|
|
@@ -294,12 +309,13 @@ Commands:
|
|
|
294
309
|
|
|
295
310
|
Examples:
|
|
296
311
|
tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d
|
|
297
|
-
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
|
|
298
314
|
tender auth login --device --profile create --shopify-store tender-prompt --json
|
|
299
315
|
tender auth status --json`;
|
|
300
316
|
}
|
|
301
317
|
function authLoginHelp() {
|
|
302
|
-
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]
|
|
303
319
|
|
|
304
320
|
Options:
|
|
305
321
|
--device Use the OAuth device-code flow.
|
|
@@ -310,12 +326,14 @@ Options:
|
|
|
310
326
|
--artifact <artifact-id> Scope the token to one artifact. Required for normal edit-preview agent work.
|
|
311
327
|
--ttl <value> Token lifetime: 7d, 14d, or 30d. Defaults to 7d.
|
|
312
328
|
--save-profile <name> Local config profile name. Defaults to default, or shopify-<store> with Shopify approval.
|
|
313
|
-
--no-poll Print the verification URL and exit without waiting
|
|
314
|
-
--
|
|
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.
|
|
315
332
|
|
|
316
333
|
Examples:
|
|
317
334
|
tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d
|
|
318
|
-
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
|
|
319
337
|
tender auth login --device --profile create --ttl 7d --save-profile account
|
|
320
338
|
tender auth login --device --profile create --shopify-store tender-prompt --json
|
|
321
339
|
tender auth login --device --profile db-read --artifact artifact_123 --ttl 7d --save-profile db`;
|
|
@@ -460,7 +478,34 @@ Examples:
|
|
|
460
478
|
tender connectors shopify source init conn_123 --dir ./shopify-app --client-id abc --profile account --json
|
|
461
479
|
tender connectors shopify source validate conn_123 --dir ./shopify-app --execute --json
|
|
462
480
|
tender connectors shopify source deploy conn_123 --dir ./shopify-app --confirm deploy --profile account --json
|
|
463
|
-
|
|
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`;
|
|
464
509
|
}
|
|
465
510
|
function connectorsShopifyCreateHelp() {
|
|
466
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]
|
|
@@ -517,19 +562,22 @@ Examples:
|
|
|
517
562
|
tender connectors shopify verify conn_123 --profile account --json`;
|
|
518
563
|
}
|
|
519
564
|
function connectorsShopifySourceInitHelp() {
|
|
520
|
-
return `Usage: tender connectors shopify source init <connector-id> --dir <path> --client-id <shopify-client-id> [--source-name <name>] [--scopes <csv>] [--force] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
565
|
+
return `Usage: tender connectors shopify source init <connector-id> --dir <path> --client-id <shopify-client-id> [--template <minimal|shopify-pdp-survey>] [--source-name <name>] [--scopes <csv>] [--force] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
521
566
|
|
|
522
567
|
Creates or reuses an internal Tender source artifact for the merchant-owned
|
|
523
|
-
Shopify app, checks it out to --dir, writes a
|
|
524
|
-
source tree, commits it, pushes it to Artifact Git, and records
|
|
568
|
+
Shopify app, checks it out to --dir, writes a product page survey app block
|
|
569
|
+
source tree by default, commits it, pushes it to Artifact Git, and records
|
|
525
570
|
sourceArtifactId on the connector.
|
|
526
571
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
572
|
+
Pass --template minimal for an empty non-secret Shopify app source tree.
|
|
573
|
+
|
|
574
|
+
The CLI bootstraps source. Additional app blocks, web pixels, customer metadata
|
|
575
|
+
definitions, and app-owned GraphQL resources are source edits guided by
|
|
576
|
+
playbooks.
|
|
530
577
|
|
|
531
578
|
Examples:
|
|
532
|
-
tender connectors shopify source init conn_123 --dir ./shopify-app --client-id abc --profile account --json
|
|
579
|
+
tender connectors shopify source init conn_123 --dir ./shopify-app --client-id abc --profile account --json
|
|
580
|
+
tender connectors shopify source init conn_123 --dir ./shopify-app --client-id abc --template minimal --profile account --json`;
|
|
533
581
|
}
|
|
534
582
|
function connectorsShopifySourceCheckoutHelp() {
|
|
535
583
|
return `Usage: tender connectors shopify source checkout <connector-id> --dir <path> [--remote <name>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
@@ -558,6 +606,13 @@ SHOPIFY_APP_AUTOMATION_TOKEN is not set locally, the CLI requests the
|
|
|
558
606
|
connector's stored App Automation Token and injects it only into the spawned
|
|
559
607
|
Shopify CLI child process.
|
|
560
608
|
|
|
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.
|
|
615
|
+
|
|
561
616
|
Examples:
|
|
562
617
|
tender connectors shopify source deploy conn_123 --dir ./shopify-app --version connector-source-smoke --confirm deploy --profile account --json`;
|
|
563
618
|
}
|
|
@@ -676,12 +731,14 @@ Commands:
|
|
|
676
731
|
Examples:
|
|
677
732
|
tender app list --json
|
|
678
733
|
tender app create --name "Exit Intent Widget" --json
|
|
679
|
-
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
|
|
680
736
|
tender app update artifact_123 --name "Sale Widget" --json
|
|
681
737
|
tender app share create artifact_123 --expires 7d --claims 1 --json
|
|
682
738
|
tender app share inspect tok_share_123 --json
|
|
683
739
|
tender app claim tok_share_123 --dir ./widget --confirm source-and-setup --json
|
|
684
|
-
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
|
|
685
742
|
tender app validate artifact_123 --json
|
|
686
743
|
tender app build artifact_123 --commit $(git rev-parse HEAD) --json
|
|
687
744
|
tender app preview artifact_123 --commit $(git rev-parse HEAD) --stream --json
|
|
@@ -1105,13 +1162,13 @@ Examples:
|
|
|
1105
1162
|
TENDER_API_TOKEN=tpat_... tender app list --base-url https://app.tenderprompt.com --json`;
|
|
1106
1163
|
}
|
|
1107
1164
|
function artifactsCreateHelp() {
|
|
1108
|
-
return `Usage: tender app create --name <name> [--init] [--dir <path>] [--scaffold <server-backed|none>] [--preview] [--target-pack <id>] [--target-pack-version <version>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
1165
|
+
return `Usage: tender app create --name <name> [--init] [--dir <path>] [--scaffold <server-backed|shopify-pdp-survey-runtime|none>] [--preview] [--target-pack <id>] [--target-pack-version <version>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
1109
1166
|
|
|
1110
1167
|
Options:
|
|
1111
1168
|
--name <name> App name.
|
|
1112
1169
|
--init Initialize a local checkout after creating the app.
|
|
1113
1170
|
--dir <path> Local checkout directory for --init. Defaults to current directory.
|
|
1114
|
-
--scaffold <value> API-derived scaffold for empty app source: server-backed or none. Defaults to server-backed.
|
|
1171
|
+
--scaffold <value> API-derived scaffold for empty app source: server-backed, shopify-pdp-survey-runtime, or none. Defaults to server-backed.
|
|
1115
1172
|
--no-scaffold Alias for --scaffold none.
|
|
1116
1173
|
--preview After --init writes and pushes initial source, request a preview build.
|
|
1117
1174
|
--remote <name> Git remote name for --init. Defaults to tender.
|
|
@@ -1126,7 +1183,8 @@ Options:
|
|
|
1126
1183
|
|
|
1127
1184
|
Examples:
|
|
1128
1185
|
tender app create --name "Exit Intent Widget" --json
|
|
1129
|
-
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
|
|
1130
1188
|
tender app create --name "Exit Intent Widget" --profile account --json`;
|
|
1131
1189
|
}
|
|
1132
1190
|
function artifactsUpdateHelp() {
|
|
@@ -1143,12 +1201,12 @@ Examples:
|
|
|
1143
1201
|
tender app update artifact_123 --name "Sale Widget" --json`;
|
|
1144
1202
|
}
|
|
1145
1203
|
function artifactsInitHelp() {
|
|
1146
|
-
return `Usage: tender app init <app-id> [--dir <path>] [--remote <name>] [--scaffold <server-backed|none>] [--preview] [--base-url <url>] [--token <token>] [--profile <name>] [--ttl <7d|14d|30d>] [--dry-run] [--force] [--json]
|
|
1204
|
+
return `Usage: tender app init <app-id> [--dir <path>] [--remote <name>] [--scaffold <server-backed|shopify-pdp-survey-runtime|none>] [--preview] [--base-url <url>] [--token <token>] [--profile <name>] [--ttl <7d|14d|30d>] [--dry-run] [--force] [--json]
|
|
1147
1205
|
|
|
1148
1206
|
Options:
|
|
1149
1207
|
--dir <path> Local checkout directory to create/update. Defaults to current directory.
|
|
1150
1208
|
--remote <name> Git remote name to add or update. Defaults to tender.
|
|
1151
|
-
--scaffold <value> API-derived scaffold for empty app source: server-backed or none. Defaults to server-backed.
|
|
1209
|
+
--scaffold <value> API-derived scaffold for empty app source: server-backed, shopify-pdp-survey-runtime, or none. Defaults to server-backed.
|
|
1152
1210
|
--no-scaffold Alias for --scaffold none.
|
|
1153
1211
|
--preview Request a preview build for the initialized source commit.
|
|
1154
1212
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
@@ -1161,8 +1219,8 @@ Options:
|
|
|
1161
1219
|
|
|
1162
1220
|
Examples:
|
|
1163
1221
|
tender app init artifact_123 --dir ./widget --dry-run --json
|
|
1164
|
-
tender app init artifact_123 --dir ./
|
|
1165
|
-
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
|
|
1166
1224
|
tender app init artifact_123 --dir ./widget --force --json`;
|
|
1167
1225
|
}
|
|
1168
1226
|
function artifactsContextFetchHelp() {
|
|
@@ -1305,10 +1363,10 @@ Examples:
|
|
|
1305
1363
|
tender app git credential artifact_123 get --remote-url https://app.tenderprompt.com/git/artifacts/artifact_123.git`;
|
|
1306
1364
|
}
|
|
1307
1365
|
function previewRebuildHelp() {
|
|
1308
|
-
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]
|
|
1309
1367
|
|
|
1310
1368
|
Options:
|
|
1311
|
-
--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.
|
|
1312
1370
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
1313
1371
|
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
1314
1372
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
@@ -1334,10 +1392,10 @@ Examples:
|
|
|
1334
1392
|
tender app validate artifact_123 --stream --json`;
|
|
1335
1393
|
}
|
|
1336
1394
|
function buildHelp() {
|
|
1337
|
-
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]
|
|
1338
1396
|
|
|
1339
1397
|
Options:
|
|
1340
|
-
--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.
|
|
1341
1399
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
1342
1400
|
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
1343
1401
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
@@ -1349,10 +1407,10 @@ Examples:
|
|
|
1349
1407
|
tender app build artifact_123 --stream --json`;
|
|
1350
1408
|
}
|
|
1351
1409
|
function previewStatusHelp() {
|
|
1352
|
-
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]
|
|
1353
1411
|
|
|
1354
1412
|
Options:
|
|
1355
|
-
--commit <sha>
|
|
1413
|
+
--commit <sha-or-ref> Show the latest preview job for one artifact Git commit. Git refs like HEAD are resolved locally.
|
|
1356
1414
|
--job <job-id> Show one lifecycle job directly.
|
|
1357
1415
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
1358
1416
|
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
@@ -1364,10 +1422,10 @@ Examples:
|
|
|
1364
1422
|
tender app preview status artifact_123 --job job_123 --json`;
|
|
1365
1423
|
}
|
|
1366
1424
|
function previewWatchHelp() {
|
|
1367
|
-
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]
|
|
1368
1426
|
|
|
1369
1427
|
Options:
|
|
1370
|
-
--commit <sha>
|
|
1428
|
+
--commit <sha-or-ref> Watch the latest preview job for one artifact Git commit. Git refs like HEAD are resolved locally.
|
|
1371
1429
|
--job <job-id> Watch one lifecycle job directly.
|
|
1372
1430
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
1373
1431
|
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
@@ -1380,12 +1438,12 @@ Examples:
|
|
|
1380
1438
|
tender app preview watch artifact_123 --job job_123 --stream --json`;
|
|
1381
1439
|
}
|
|
1382
1440
|
function publishHelp() {
|
|
1383
|
-
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]
|
|
1384
1442
|
|
|
1385
1443
|
Options:
|
|
1386
1444
|
--dry-run Show the publish request without calling the API.
|
|
1387
1445
|
--confirm publish Required to call the production-facing publish API.
|
|
1388
|
-
--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.
|
|
1389
1447
|
--message <text> Human audit message returned in CLI output.
|
|
1390
1448
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
1391
1449
|
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
@@ -1494,7 +1552,7 @@ function tenderCliCapabilities() {
|
|
|
1494
1552
|
cli: {
|
|
1495
1553
|
name: CLI_PACKAGE_NAME,
|
|
1496
1554
|
version: CLI_PACKAGE_VERSION,
|
|
1497
|
-
latestRunner: 'npm exec --yes @tenderprompt/cli@latest --',
|
|
1555
|
+
latestRunner: 'npm exec --yes --package @tenderprompt/cli@latest -- tender',
|
|
1498
1556
|
},
|
|
1499
1557
|
recommendedSkill: RECOMMENDED_TENDER_PROMPT_SKILL,
|
|
1500
1558
|
playbooks: {
|
|
@@ -1560,7 +1618,7 @@ function tenderCliCapabilities() {
|
|
|
1560
1618
|
},
|
|
1561
1619
|
description: 'Agent-safe Tender App source, lifecycle, analytics, and local scaffold controls.',
|
|
1562
1620
|
discovery: {
|
|
1563
|
-
primary: 'npm exec --yes @tenderprompt/cli@latest -- capabilities --json',
|
|
1621
|
+
primary: 'npm exec --yes --package @tenderprompt/cli@latest -- tender capabilities --json',
|
|
1564
1622
|
layeredHelp: [
|
|
1565
1623
|
'tender --version',
|
|
1566
1624
|
'tender --help',
|
|
@@ -1578,6 +1636,7 @@ function tenderCliCapabilities() {
|
|
|
1578
1636
|
'tender app outbound-auth --help',
|
|
1579
1637
|
'tender connectors --help',
|
|
1580
1638
|
'tender connectors shopify source init --help',
|
|
1639
|
+
'tender shopify workspace --help',
|
|
1581
1640
|
],
|
|
1582
1641
|
},
|
|
1583
1642
|
workflows: [
|
|
@@ -1586,7 +1645,8 @@ function tenderCliCapabilities() {
|
|
|
1586
1645
|
when: 'Before reading or changing remote Tender App records.',
|
|
1587
1646
|
commands: [
|
|
1588
1647
|
'tender auth status --json',
|
|
1589
|
-
'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',
|
|
1590
1650
|
],
|
|
1591
1651
|
},
|
|
1592
1652
|
{
|
|
@@ -1621,12 +1681,46 @@ function tenderCliCapabilities() {
|
|
|
1621
1681
|
'Fetch shopify-connector-customer-metadata-widget when the requested job involves customer metafields, metaobjects, or profile writes.',
|
|
1622
1682
|
],
|
|
1623
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
|
+
},
|
|
1624
1714
|
{
|
|
1625
1715
|
name: 'create_new_app_with_preview',
|
|
1626
1716
|
when: 'When the user wants a new Tender App that is immediately visible.',
|
|
1627
|
-
commands: [
|
|
1717
|
+
commands: [
|
|
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',
|
|
1720
|
+
],
|
|
1628
1721
|
notes: [
|
|
1629
|
-
'create --init
|
|
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.',
|
|
1630
1724
|
'--preview requests a preview build for the initial source commit after the push.',
|
|
1631
1725
|
],
|
|
1632
1726
|
},
|
|
@@ -1721,6 +1815,7 @@ function tenderCliCapabilities() {
|
|
|
1721
1815
|
when: 'When inspecting app-local database tables from a scoped token.',
|
|
1722
1816
|
commands: [
|
|
1723
1817
|
'tender auth login --device --profile db-read --artifact <artifact-id> --ttl 7d --save-profile db --json',
|
|
1818
|
+
'tender auth status --profile db --json',
|
|
1724
1819
|
'tender app db capabilities <artifact-id> --profile db --json',
|
|
1725
1820
|
'tender app db tables <artifact-id> --env published --profile db --json',
|
|
1726
1821
|
'tender app db schema <artifact-id> --env published --table <table-name> --profile db --json',
|
|
@@ -1789,7 +1884,7 @@ function tenderCliCapabilities() {
|
|
|
1789
1884
|
},
|
|
1790
1885
|
{
|
|
1791
1886
|
command: 'tender app create --name <name> --init --dir <dir> --preview --json',
|
|
1792
|
-
purpose: 'Create a new Tender App, initialize
|
|
1887
|
+
purpose: 'Create a new Tender App, initialize the default server-backed source in a local checkout, push it, and request a preview build.',
|
|
1793
1888
|
requiresAuth: true,
|
|
1794
1889
|
writes: true,
|
|
1795
1890
|
dryRun: false,
|
|
@@ -2040,6 +2135,45 @@ function normalizeStoredProfile(value) {
|
|
|
2040
2135
|
...normalizeStoredShopifyContext(record),
|
|
2041
2136
|
};
|
|
2042
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
|
+
}
|
|
2043
2177
|
function normalizeStoredShopifyContext(record) {
|
|
2044
2178
|
const shopifyAppUrl = typeof record.shopifyAppUrl === 'string' ? record.shopifyAppUrl : null;
|
|
2045
2179
|
if (shopifyAppUrl) {
|
|
@@ -2083,9 +2217,21 @@ function normalizeConfig(value) {
|
|
|
2083
2217
|
}
|
|
2084
2218
|
}
|
|
2085
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
|
+
}
|
|
2086
2231
|
return {
|
|
2087
2232
|
defaultProfile: typeof record.defaultProfile === 'string' && record.defaultProfile ? record.defaultProfile : undefined,
|
|
2088
2233
|
profiles: Object.keys(profiles).length > 0 ? profiles : undefined,
|
|
2234
|
+
pendingDeviceAuthorizations: Object.keys(pendingDeviceAuthorizations).length > 0 ? pendingDeviceAuthorizations : undefined,
|
|
2089
2235
|
};
|
|
2090
2236
|
}
|
|
2091
2237
|
async function readConfig(env) {
|
|
@@ -2152,6 +2298,30 @@ async function resolveApiCredentials(input) {
|
|
|
2152
2298
|
profileName,
|
|
2153
2299
|
};
|
|
2154
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
|
+
}
|
|
2155
2325
|
function parseProfileName(value, flag) {
|
|
2156
2326
|
if (!value || value.startsWith('-')) {
|
|
2157
2327
|
throw new TenderCliUsageError(`${flag} requires a profile name.`);
|
|
@@ -2233,8 +2403,8 @@ function parseTtl(value) {
|
|
|
2233
2403
|
return value;
|
|
2234
2404
|
}
|
|
2235
2405
|
function parseScaffoldTemplate(value) {
|
|
2236
|
-
if (value !== 'server-backed' && value !== 'none') {
|
|
2237
|
-
throw new TenderCliUsageError('--scaffold must be one of server-backed or none.');
|
|
2406
|
+
if (value !== 'server-backed' && value !== 'shopify-pdp-survey-runtime' && value !== 'none') {
|
|
2407
|
+
throw new TenderCliUsageError('--scaffold must be one of server-backed, shopify-pdp-survey-runtime, or none.');
|
|
2238
2408
|
}
|
|
2239
2409
|
return value;
|
|
2240
2410
|
}
|
|
@@ -2542,7 +2712,7 @@ function parseBuildArgs(args) {
|
|
|
2542
2712
|
if (arg === '--commit') {
|
|
2543
2713
|
const value = args[index + 1];
|
|
2544
2714
|
if (!value || value.startsWith('-')) {
|
|
2545
|
-
throw new TenderCliUsageError('--commit requires a Git commit SHA.');
|
|
2715
|
+
throw new TenderCliUsageError('--commit requires a Git commit SHA or Git ref.');
|
|
2546
2716
|
}
|
|
2547
2717
|
commitSha = value;
|
|
2548
2718
|
index += 1;
|
|
@@ -2603,7 +2773,7 @@ function parsePreviewRebuildArgs(args) {
|
|
|
2603
2773
|
if (arg === '--commit') {
|
|
2604
2774
|
const value = args[index + 1];
|
|
2605
2775
|
if (!value || value.startsWith('-')) {
|
|
2606
|
-
throw new TenderCliUsageError('--commit requires a Git commit SHA.');
|
|
2776
|
+
throw new TenderCliUsageError('--commit requires a Git commit SHA or Git ref.');
|
|
2607
2777
|
}
|
|
2608
2778
|
commitSha = value;
|
|
2609
2779
|
index += 1;
|
|
@@ -2658,7 +2828,7 @@ function readPreviewLookupOptions(input) {
|
|
|
2658
2828
|
if (arg === '--commit') {
|
|
2659
2829
|
const value = input.args[index + 1];
|
|
2660
2830
|
if (!value || value.startsWith('-')) {
|
|
2661
|
-
throw new TenderCliUsageError('--commit requires a commit SHA.');
|
|
2831
|
+
throw new TenderCliUsageError('--commit requires a Git commit SHA or Git ref.');
|
|
2662
2832
|
}
|
|
2663
2833
|
commitSha = value;
|
|
2664
2834
|
index += 1;
|
|
@@ -2807,7 +2977,7 @@ function parsePublishArgs(args) {
|
|
|
2807
2977
|
if (arg === '--commit') {
|
|
2808
2978
|
const value = args[index + 1];
|
|
2809
2979
|
if (!value || value.startsWith('-')) {
|
|
2810
|
-
throw new TenderCliUsageError('--commit requires a commit SHA.');
|
|
2980
|
+
throw new TenderCliUsageError('--commit requires a Git commit SHA or Git ref.');
|
|
2811
2981
|
}
|
|
2812
2982
|
commitSha = value;
|
|
2813
2983
|
index += 1;
|
|
@@ -3198,7 +3368,8 @@ function parseArtifactsCreateArgs(args) {
|
|
|
3198
3368
|
let remoteName = 'tender';
|
|
3199
3369
|
let ttl = '7d';
|
|
3200
3370
|
let force = false;
|
|
3201
|
-
let scaffold =
|
|
3371
|
+
let scaffold = DEFAULT_TENDER_APP_SCAFFOLD_TEMPLATE;
|
|
3372
|
+
let scaffoldSpecified = false;
|
|
3202
3373
|
let preview = false;
|
|
3203
3374
|
let baseUrl = null;
|
|
3204
3375
|
let flagToken = null;
|
|
@@ -3224,6 +3395,7 @@ function parseArtifactsCreateArgs(args) {
|
|
|
3224
3395
|
}
|
|
3225
3396
|
if (arg === '--no-scaffold') {
|
|
3226
3397
|
scaffold = 'none';
|
|
3398
|
+
scaffoldSpecified = true;
|
|
3227
3399
|
continue;
|
|
3228
3400
|
}
|
|
3229
3401
|
if (arg === '--name') {
|
|
@@ -3274,6 +3446,7 @@ function parseArtifactsCreateArgs(args) {
|
|
|
3274
3446
|
}
|
|
3275
3447
|
if (arg === '--scaffold') {
|
|
3276
3448
|
scaffold = parseScaffoldTemplate(args[index + 1]);
|
|
3449
|
+
scaffoldSpecified = true;
|
|
3277
3450
|
index += 1;
|
|
3278
3451
|
continue;
|
|
3279
3452
|
}
|
|
@@ -3299,7 +3472,7 @@ function parseArtifactsCreateArgs(args) {
|
|
|
3299
3472
|
}
|
|
3300
3473
|
if (!init) {
|
|
3301
3474
|
if (preview) {
|
|
3302
|
-
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');
|
|
3303
3476
|
}
|
|
3304
3477
|
if (force) {
|
|
3305
3478
|
throw new TenderCliUsageError('--force is only valid with --init.');
|
|
@@ -3313,7 +3486,7 @@ function parseArtifactsCreateArgs(args) {
|
|
|
3313
3486
|
if (ttl !== '7d') {
|
|
3314
3487
|
throw new TenderCliUsageError('--ttl is only valid with --init.');
|
|
3315
3488
|
}
|
|
3316
|
-
if (
|
|
3489
|
+
if (scaffoldSpecified) {
|
|
3317
3490
|
throw new TenderCliUsageError('--scaffold is only valid with --init.');
|
|
3318
3491
|
}
|
|
3319
3492
|
}
|
|
@@ -3684,6 +3857,7 @@ function parseConnectorsShopifySourceInitArgs(args) {
|
|
|
3684
3857
|
let clientId = null;
|
|
3685
3858
|
let sourceName = null;
|
|
3686
3859
|
let scopes = null;
|
|
3860
|
+
let template = DEFAULT_SHOPIFY_CONNECTOR_SOURCE_TEMPLATE;
|
|
3687
3861
|
let force = false;
|
|
3688
3862
|
for (let index = 0; index < parsed.remaining.length; index += 1) {
|
|
3689
3863
|
const arg = parsed.remaining[index];
|
|
@@ -3707,6 +3881,11 @@ function parseConnectorsShopifySourceInitArgs(args) {
|
|
|
3707
3881
|
index += 1;
|
|
3708
3882
|
continue;
|
|
3709
3883
|
}
|
|
3884
|
+
if (arg === '--template') {
|
|
3885
|
+
template = parseShopifyConnectorSourceTemplate(parsed.remaining[index + 1]);
|
|
3886
|
+
index += 1;
|
|
3887
|
+
continue;
|
|
3888
|
+
}
|
|
3710
3889
|
if (arg === '--force') {
|
|
3711
3890
|
force = true;
|
|
3712
3891
|
continue;
|
|
@@ -3726,6 +3905,7 @@ function parseConnectorsShopifySourceInitArgs(args) {
|
|
|
3726
3905
|
clientId,
|
|
3727
3906
|
sourceName,
|
|
3728
3907
|
scopes,
|
|
3908
|
+
template,
|
|
3729
3909
|
force,
|
|
3730
3910
|
...parsed,
|
|
3731
3911
|
};
|
|
@@ -3839,6 +4019,58 @@ function parseConnectorsShopifySourceDeployArgs(args) {
|
|
|
3839
4019
|
}
|
|
3840
4020
|
return { command: 'connectors shopify source deploy', connectorId, dir, clientId, version, message, sourceControlUrl, allowDeletes, confirm, ...parsed };
|
|
3841
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
|
+
}
|
|
3842
4074
|
function parseConnectorsShopifyDeleteArgs(args) {
|
|
3843
4075
|
if (args.includes('--help') || args.includes('-h')) {
|
|
3844
4076
|
return { command: 'help', topic: 'connectors shopify delete' };
|
|
@@ -3991,7 +4223,7 @@ function parseArtifactsInitArgs(args) {
|
|
|
3991
4223
|
let flagToken = null;
|
|
3992
4224
|
let profileName = null;
|
|
3993
4225
|
let ttl = '7d';
|
|
3994
|
-
let scaffold =
|
|
4226
|
+
let scaffold = DEFAULT_TENDER_APP_SCAFFOLD_TEMPLATE;
|
|
3995
4227
|
let preview = false;
|
|
3996
4228
|
let dryRun = false;
|
|
3997
4229
|
let force = false;
|
|
@@ -4494,6 +4726,7 @@ function parseAuthLoginArgs(args) {
|
|
|
4494
4726
|
let ttl = '7d';
|
|
4495
4727
|
let json = false;
|
|
4496
4728
|
let noPoll = false;
|
|
4729
|
+
let poll = false;
|
|
4497
4730
|
let pollTimeoutSeconds = 600;
|
|
4498
4731
|
for (let index = 0; index < args.length; index += 1) {
|
|
4499
4732
|
const arg = args[index];
|
|
@@ -4506,9 +4739,19 @@ function parseAuthLoginArgs(args) {
|
|
|
4506
4739
|
continue;
|
|
4507
4740
|
}
|
|
4508
4741
|
if (arg === '--no-poll') {
|
|
4742
|
+
if (poll) {
|
|
4743
|
+
throw new TenderCliUsageError('--no-poll cannot be combined with --poll.');
|
|
4744
|
+
}
|
|
4509
4745
|
noPoll = true;
|
|
4510
4746
|
continue;
|
|
4511
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
|
+
}
|
|
4512
4755
|
if (arg === '--base-url') {
|
|
4513
4756
|
baseUrl = parseBaseUrlFlag(args, index);
|
|
4514
4757
|
index += 1;
|
|
@@ -4555,6 +4798,10 @@ function parseAuthLoginArgs(args) {
|
|
|
4555
4798
|
continue;
|
|
4556
4799
|
}
|
|
4557
4800
|
if (arg === '--poll-timeout') {
|
|
4801
|
+
if (noPoll) {
|
|
4802
|
+
throw new TenderCliUsageError('--poll-timeout cannot be combined with --no-poll.');
|
|
4803
|
+
}
|
|
4804
|
+
poll = true;
|
|
4558
4805
|
pollTimeoutSeconds = parsePositiveIntegerFlag(args[index + 1], '--poll-timeout');
|
|
4559
4806
|
index += 1;
|
|
4560
4807
|
continue;
|
|
@@ -4584,7 +4831,7 @@ function parseAuthLoginArgs(args) {
|
|
|
4584
4831
|
artifactId,
|
|
4585
4832
|
ttl,
|
|
4586
4833
|
json,
|
|
4587
|
-
noPoll,
|
|
4834
|
+
noPoll: noPoll || (json && !poll),
|
|
4588
4835
|
pollTimeoutSeconds,
|
|
4589
4836
|
};
|
|
4590
4837
|
}
|
|
@@ -6919,6 +7166,30 @@ function parseTenderArgs(args) {
|
|
|
6919
7166
|
}
|
|
6920
7167
|
throw new TenderCliUsageError(`Unknown connectors command: ${args[1]}`);
|
|
6921
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
|
+
}
|
|
6922
7193
|
if (args[0] === 'apps') {
|
|
6923
7194
|
throw new TenderCliUsageError('Unknown command: apps. Use `tender app ...` for Tender App workflows.');
|
|
6924
7195
|
}
|
|
@@ -7629,7 +7900,7 @@ async function requestPlaybooksApi(input) {
|
|
|
7629
7900
|
(typeof payload?.reasonCode === 'string' && payload.reasonCode) ||
|
|
7630
7901
|
`Playbooks request failed with HTTP ${response.status}.`;
|
|
7631
7902
|
const authHint = response.status === 401
|
|
7632
|
-
? '\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.'
|
|
7633
7904
|
: '';
|
|
7634
7905
|
throw new Error([message, `Reason: ${reasonCode}.`, 'Example: tender playbooks list --profile agent --json.'].join('\n') + authHint);
|
|
7635
7906
|
}
|
|
@@ -7637,13 +7908,16 @@ async function requestPlaybooksApi(input) {
|
|
|
7637
7908
|
}
|
|
7638
7909
|
async function requestConnectorsApi(input) {
|
|
7639
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
|
+
}
|
|
7640
7918
|
const response = await input.fetcher(`${input.baseUrl}/api/account/connectors${suffix}`, {
|
|
7641
7919
|
method: input.method,
|
|
7642
|
-
headers
|
|
7643
|
-
authorization: `Bearer ${input.token}`,
|
|
7644
|
-
accept: 'application/json',
|
|
7645
|
-
...(input.body === undefined ? {} : { 'content-type': 'application/json' }),
|
|
7646
|
-
},
|
|
7920
|
+
headers,
|
|
7647
7921
|
body: input.body === undefined ? undefined : JSON.stringify(input.body),
|
|
7648
7922
|
});
|
|
7649
7923
|
const payload = response.status === 204 ? { ok: true } : await parseJsonResponse(response);
|
|
@@ -7652,10 +7926,189 @@ async function requestConnectorsApi(input) {
|
|
|
7652
7926
|
const message = (typeof payload?.message === 'string' && payload.message) ||
|
|
7653
7927
|
(typeof payload?.reasonCode === 'string' && payload.reasonCode) ||
|
|
7654
7928
|
`Connector request failed with HTTP ${response.status}.`;
|
|
7655
|
-
|
|
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(/\.$/, ''));
|
|
7656
7933
|
}
|
|
7657
7934
|
return payload;
|
|
7658
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
|
+
}
|
|
7659
8112
|
function dbCorrectedExample(artifactId, dbPath) {
|
|
7660
8113
|
if (dbPath === 'capabilities') {
|
|
7661
8114
|
return `tender app db capabilities ${artifactId} --json`;
|
|
@@ -8262,10 +8715,208 @@ async function planSourceFiles(input) {
|
|
|
8262
8715
|
}
|
|
8263
8716
|
return planned.sort((left, right) => left.path.localeCompare(right.path));
|
|
8264
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
|
+
}
|
|
8265
8900
|
async function runAuthStatus(command, io, runtime) {
|
|
8266
8901
|
const env = runtime.env ?? process.env;
|
|
8902
|
+
const fetcher = runtime.fetcher ?? fetch;
|
|
8903
|
+
const now = runtime.now ?? Date.now;
|
|
8267
8904
|
if (command.profileName) {
|
|
8268
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
|
+
}
|
|
8269
8920
|
const profile = config.profiles?.[command.profileName] ?? null;
|
|
8270
8921
|
const result = {
|
|
8271
8922
|
ok: Boolean(profile),
|
|
@@ -8327,6 +8978,20 @@ async function runAuthStatus(command, io, runtime) {
|
|
|
8327
8978
|
}
|
|
8328
8979
|
const config = await readConfig(env);
|
|
8329
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
|
+
}
|
|
8330
8995
|
const profile = config.profiles?.[profileName] ?? null;
|
|
8331
8996
|
const result = {
|
|
8332
8997
|
ok: Boolean(profile),
|
|
@@ -8391,7 +9056,11 @@ async function runAuthLogin(command, io, runtime) {
|
|
|
8391
9056
|
}),
|
|
8392
9057
|
});
|
|
8393
9058
|
const devicePayload = await parseJsonResponse(deviceResponse);
|
|
8394
|
-
if (!deviceResponse.ok ||
|
|
9059
|
+
if (!deviceResponse.ok ||
|
|
9060
|
+
!devicePayload?.device_code ||
|
|
9061
|
+
!devicePayload.user_code ||
|
|
9062
|
+
!devicePayload.verification_uri ||
|
|
9063
|
+
!devicePayload.verification_uri_complete) {
|
|
8395
9064
|
throw new Error(devicePayload?.error_description ?? devicePayload?.error ?? `Device authorization failed with HTTP ${deviceResponse.status}.`);
|
|
8396
9065
|
}
|
|
8397
9066
|
const verificationUriComplete = shopifyAppUrl
|
|
@@ -8401,19 +9070,51 @@ async function runAuthLogin(command, io, runtime) {
|
|
|
8401
9070
|
})
|
|
8402
9071
|
: devicePayload.verification_uri_complete;
|
|
8403
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);
|
|
8404
9097
|
const pendingResult = {
|
|
8405
|
-
ok:
|
|
9098
|
+
ok: false,
|
|
8406
9099
|
command: command.command,
|
|
9100
|
+
status: 'authorization_pending',
|
|
9101
|
+
authenticated: false,
|
|
9102
|
+
pending: true,
|
|
8407
9103
|
baseUrl,
|
|
8408
9104
|
shopifyAppUrl,
|
|
8409
9105
|
shopifyStore,
|
|
8410
9106
|
profile: command.tokenProfile,
|
|
8411
9107
|
saveProfile: command.saveProfile,
|
|
9108
|
+
pendingProfile: command.saveProfile,
|
|
9109
|
+
artifactId: command.artifactId,
|
|
8412
9110
|
userCode: devicePayload.user_code,
|
|
8413
9111
|
verificationUri,
|
|
8414
9112
|
verificationUriComplete,
|
|
8415
9113
|
expiresIn: devicePayload.expires_in ?? null,
|
|
9114
|
+
expiresAt: formatExpiry(expiresAtMs),
|
|
8416
9115
|
interval: devicePayload.interval ?? null,
|
|
9116
|
+
configPath: readConfigPath(env),
|
|
9117
|
+
nextCommand: buildAuthStatusNextCommand(command.saveProfile),
|
|
8417
9118
|
};
|
|
8418
9119
|
if (command.noPoll) {
|
|
8419
9120
|
if (command.json) {
|
|
@@ -8424,14 +9125,16 @@ async function runAuthLogin(command, io, runtime) {
|
|
|
8424
9125
|
}
|
|
8425
9126
|
return 0;
|
|
8426
9127
|
}
|
|
8427
|
-
if (
|
|
9128
|
+
if (command.json) {
|
|
9129
|
+
io.stderr(JSON.stringify(pendingResult, null, 2));
|
|
9130
|
+
}
|
|
9131
|
+
else {
|
|
8428
9132
|
io.stdout([
|
|
8429
9133
|
`Open this URL to approve Tender CLI access: ${verificationUriComplete}`,
|
|
8430
9134
|
`User code: ${devicePayload.user_code}`,
|
|
8431
9135
|
].join('\n'));
|
|
8432
9136
|
}
|
|
8433
9137
|
let intervalSeconds = Math.max(1, devicePayload.interval ?? 5);
|
|
8434
|
-
const expiresAtMs = now() + Math.max(1, devicePayload.expires_in ?? 600) * 1000;
|
|
8435
9138
|
let attemptsRemaining = Math.max(1, Math.ceil(command.pollTimeoutSeconds / intervalSeconds));
|
|
8436
9139
|
while (attemptsRemaining > 0 && now() < expiresAtMs) {
|
|
8437
9140
|
attemptsRemaining -= 1;
|
|
@@ -8447,38 +9150,29 @@ async function runAuthLogin(command, io, runtime) {
|
|
|
8447
9150
|
});
|
|
8448
9151
|
const tokenPayload = await parseJsonResponse(tokenResponse);
|
|
8449
9152
|
if (tokenResponse.ok && tokenPayload?.access_token) {
|
|
8450
|
-
const profiles =
|
|
8451
|
-
profiles[command.saveProfile] = {
|
|
9153
|
+
const profiles = configWithPendingAuthorization.profiles ?? {};
|
|
9154
|
+
profiles[command.saveProfile] = storedProfileFromOAuthToken({
|
|
8452
9155
|
baseUrl,
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
artifactId: tokenPayload.artifact_id,
|
|
8457
|
-
expiresAt: tokenPayload.expires_at,
|
|
8458
|
-
permissions: splitScope(tokenPayload.scope),
|
|
8459
|
-
...(shopifyAppUrl ? { shopifyAppUrl } : {}),
|
|
8460
|
-
...(shopifyStore ? { shopifyStore } : {}),
|
|
8461
|
-
};
|
|
8462
|
-
await writeConfig(env, {
|
|
8463
|
-
...config,
|
|
8464
|
-
defaultProfile: config.defaultProfile ?? command.saveProfile,
|
|
8465
|
-
profiles,
|
|
9156
|
+
tokenPayload,
|
|
9157
|
+
shopifyAppUrl,
|
|
9158
|
+
shopifyStore,
|
|
8466
9159
|
});
|
|
8467
|
-
const
|
|
8468
|
-
|
|
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({
|
|
8469
9167
|
command: command.command,
|
|
8470
9168
|
baseUrl,
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
tenantId: tokenPayload.tenant_id ?? null,
|
|
8475
|
-
artifactId: tokenPayload.artifact_id ?? null,
|
|
8476
|
-
permissions: splitScope(tokenPayload.scope),
|
|
8477
|
-
expiresAt: formatExpiry(tokenPayload.expires_at),
|
|
9169
|
+
tokenProfile: command.tokenProfile,
|
|
9170
|
+
saveProfile: command.saveProfile,
|
|
9171
|
+
tokenPayload,
|
|
8478
9172
|
shopifyAppUrl,
|
|
8479
9173
|
shopifyStore,
|
|
8480
9174
|
configPath: readConfigPath(env),
|
|
8481
|
-
};
|
|
9175
|
+
});
|
|
8482
9176
|
if (command.json) {
|
|
8483
9177
|
io.stdout(JSON.stringify(result, null, 2));
|
|
8484
9178
|
}
|
|
@@ -9116,6 +9810,48 @@ async function runGitCommand(input) {
|
|
|
9116
9810
|
}
|
|
9117
9811
|
return result;
|
|
9118
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
|
+
}
|
|
9119
9855
|
function shellQuote(value) {
|
|
9120
9856
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
9121
9857
|
}
|
|
@@ -9976,29 +10712,30 @@ async function runArtifactsInit(command, io, runtime) {
|
|
|
9976
10712
|
return 0;
|
|
9977
10713
|
}
|
|
9978
10714
|
async function postLifecycle(input) {
|
|
9979
|
-
const
|
|
9980
|
-
|
|
9981
|
-
|
|
9982
|
-
|
|
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,
|
|
9983
10720
|
env: input.runtime.env ?? process.env,
|
|
9984
10721
|
});
|
|
9985
10722
|
const fetcher = input.runtime.fetcher ?? fetch;
|
|
9986
|
-
const stream =
|
|
10723
|
+
const stream = command.stream;
|
|
9987
10724
|
const url = `${credentials.baseUrl}${input.path}${stream ? '?stream=1' : ''}`;
|
|
9988
10725
|
const requestBody = {};
|
|
9989
|
-
if ('commitSha' in
|
|
9990
|
-
requestBody.commitSha =
|
|
10726
|
+
if ('commitSha' in command && command.commitSha) {
|
|
10727
|
+
requestBody.commitSha = command.commitSha;
|
|
9991
10728
|
}
|
|
9992
|
-
if ('message' in
|
|
9993
|
-
requestBody.message =
|
|
10729
|
+
if ('message' in command && command.message) {
|
|
10730
|
+
requestBody.message = command.message;
|
|
9994
10731
|
}
|
|
9995
10732
|
const response = await fetcher(url, {
|
|
9996
10733
|
method: 'POST',
|
|
9997
|
-
headers: {
|
|
9998
|
-
|
|
10734
|
+
headers: apiHeadersForCredentials({
|
|
10735
|
+
credentials,
|
|
9999
10736
|
accept: stream ? 'text/event-stream' : 'application/json',
|
|
10000
|
-
|
|
10001
|
-
},
|
|
10737
|
+
jsonBody: Object.keys(requestBody).length > 0,
|
|
10738
|
+
}),
|
|
10002
10739
|
body: Object.keys(requestBody).length > 0 ? JSON.stringify(requestBody) : undefined,
|
|
10003
10740
|
});
|
|
10004
10741
|
if (stream) {
|
|
@@ -10008,19 +10745,19 @@ async function postLifecycle(input) {
|
|
|
10008
10745
|
await emitSseLifecycleStream({
|
|
10009
10746
|
response,
|
|
10010
10747
|
io: input.io,
|
|
10011
|
-
json:
|
|
10748
|
+
json: command.json,
|
|
10012
10749
|
});
|
|
10013
10750
|
await appendAutomaticMemory({
|
|
10014
10751
|
repoPath: process.cwd(),
|
|
10015
|
-
artifactId:
|
|
10752
|
+
artifactId: command.artifactId,
|
|
10016
10753
|
kind: input.operation === 'publish' ? 'change' : 'validation',
|
|
10017
|
-
summary: `${
|
|
10754
|
+
summary: `${command.command} streamed`,
|
|
10018
10755
|
body: `Operation: ${input.operation}\nStatus: streamed`,
|
|
10019
10756
|
tags: [input.operation, 'stream'],
|
|
10020
10757
|
status: 'resolved',
|
|
10021
10758
|
related: {
|
|
10022
|
-
commands: [
|
|
10023
|
-
...('commitSha' in
|
|
10759
|
+
commands: [command.command],
|
|
10760
|
+
...('commitSha' in command && command.commitSha ? { releaseId: command.commitSha } : {}),
|
|
10024
10761
|
},
|
|
10025
10762
|
runtime: input.runtime,
|
|
10026
10763
|
});
|
|
@@ -10038,12 +10775,12 @@ async function postLifecycle(input) {
|
|
|
10038
10775
|
}
|
|
10039
10776
|
const result = {
|
|
10040
10777
|
ok: true,
|
|
10041
|
-
command:
|
|
10778
|
+
command: command.command,
|
|
10042
10779
|
baseUrl: credentials.baseUrl,
|
|
10043
10780
|
authSource: credentials.source,
|
|
10044
10781
|
profile: credentials.profileName,
|
|
10045
|
-
artifactId:
|
|
10046
|
-
appId:
|
|
10782
|
+
artifactId: command.artifactId,
|
|
10783
|
+
appId: command.artifactId,
|
|
10047
10784
|
operation: payload.operation ?? input.operation,
|
|
10048
10785
|
job: payload.job ?? null,
|
|
10049
10786
|
result: payload.result ?? null,
|
|
@@ -10053,15 +10790,15 @@ async function postLifecycle(input) {
|
|
|
10053
10790
|
const releaseGitRef = input.operation === 'publish' ? readReleaseGitRefFromResult(result.result) : null;
|
|
10054
10791
|
const memory = await appendAutomaticMemory({
|
|
10055
10792
|
repoPath,
|
|
10056
|
-
artifactId:
|
|
10793
|
+
artifactId: command.artifactId,
|
|
10057
10794
|
kind: input.operation === 'validate' ? 'validation' : input.operation === 'publish' ? 'change' : 'validation',
|
|
10058
|
-
summary: `${
|
|
10795
|
+
summary: `${command.command} completed`,
|
|
10059
10796
|
body: `Operation: ${result.operation}\nStatus: completed`,
|
|
10060
10797
|
tags: [input.operation],
|
|
10061
10798
|
status: 'resolved',
|
|
10062
10799
|
related: {
|
|
10063
10800
|
files: changedFiles,
|
|
10064
|
-
commands: [
|
|
10801
|
+
commands: [command.command],
|
|
10065
10802
|
...(input.operation === 'preview' ? { previewUrl: readPreviewUrlFromJob(result.job) ?? undefined } : {}),
|
|
10066
10803
|
...(input.operation === 'publish' ? { publishedUrl: readObjectString(readObject(result.result), 'publishedUrl') ?? undefined } : {}),
|
|
10067
10804
|
...(releaseGitRef?.tagName ? { releaseId: releaseGitRef.tagName } : {}),
|
|
@@ -10076,12 +10813,12 @@ async function postLifecycle(input) {
|
|
|
10076
10813
|
memory,
|
|
10077
10814
|
...(memorySync ? { memorySync } : {}),
|
|
10078
10815
|
};
|
|
10079
|
-
if (
|
|
10816
|
+
if (command.json) {
|
|
10080
10817
|
input.io.stdout(JSON.stringify(resultWithMemory, null, 2));
|
|
10081
10818
|
}
|
|
10082
10819
|
else {
|
|
10083
10820
|
input.io.stdout([
|
|
10084
|
-
`app_id: ${
|
|
10821
|
+
`app_id: ${command.artifactId}`,
|
|
10085
10822
|
`operation: ${result.operation}`,
|
|
10086
10823
|
'status: completed',
|
|
10087
10824
|
...(input.operation === 'publish' ? formatPublishResultLines(result.result) : []),
|
|
@@ -10176,35 +10913,36 @@ async function requestPreviewStatus(input) {
|
|
|
10176
10913
|
};
|
|
10177
10914
|
}
|
|
10178
10915
|
async function runPreviewStatus(command, io, runtime) {
|
|
10916
|
+
const resolvedCommand = await resolveCommandCommitSha(command, runtime);
|
|
10179
10917
|
const credentials = await resolveApiCredentials({
|
|
10180
|
-
baseUrl:
|
|
10181
|
-
flagToken:
|
|
10182
|
-
profileName:
|
|
10918
|
+
baseUrl: resolvedCommand.baseUrl,
|
|
10919
|
+
flagToken: resolvedCommand.flagToken,
|
|
10920
|
+
profileName: resolvedCommand.profileName,
|
|
10183
10921
|
env: runtime.env ?? process.env,
|
|
10184
10922
|
});
|
|
10185
10923
|
const { job, preview } = await requestPreviewStatus({
|
|
10186
|
-
command,
|
|
10924
|
+
command: resolvedCommand,
|
|
10187
10925
|
credentials,
|
|
10188
10926
|
fetcher: runtime.fetcher ?? fetch,
|
|
10189
10927
|
});
|
|
10190
10928
|
const result = {
|
|
10191
10929
|
ok: true,
|
|
10192
|
-
command:
|
|
10930
|
+
command: resolvedCommand.command,
|
|
10193
10931
|
baseUrl: credentials.baseUrl,
|
|
10194
10932
|
authSource: credentials.source,
|
|
10195
10933
|
profile: credentials.profileName,
|
|
10196
|
-
artifactId:
|
|
10197
|
-
commitSha: preview.commitSha ??
|
|
10934
|
+
artifactId: resolvedCommand.artifactId,
|
|
10935
|
+
commitSha: preview.commitSha ?? resolvedCommand.commitSha,
|
|
10198
10936
|
status: preview.status ?? job.status ?? null,
|
|
10199
10937
|
previewUrl: preview.previewUrl ?? readPreviewUrlFromJob(job),
|
|
10200
10938
|
job,
|
|
10201
10939
|
};
|
|
10202
|
-
if (
|
|
10940
|
+
if (resolvedCommand.json) {
|
|
10203
10941
|
io.stdout(JSON.stringify(result, null, 2));
|
|
10204
10942
|
}
|
|
10205
10943
|
else {
|
|
10206
10944
|
io.stdout([
|
|
10207
|
-
`app_id: ${
|
|
10945
|
+
`app_id: ${resolvedCommand.artifactId}`,
|
|
10208
10946
|
`job_id: ${job.jobId}`,
|
|
10209
10947
|
`status: ${result.status ?? 'unknown'}`,
|
|
10210
10948
|
...(result.previewUrl ? [`preview_url: ${result.previewUrl}`] : []),
|
|
@@ -10213,19 +10951,20 @@ async function runPreviewStatus(command, io, runtime) {
|
|
|
10213
10951
|
return 0;
|
|
10214
10952
|
}
|
|
10215
10953
|
async function runPreviewWatch(command, io, runtime) {
|
|
10954
|
+
const resolvedCommand = await resolveCommandCommitSha(command, runtime);
|
|
10216
10955
|
const credentials = await resolveApiCredentials({
|
|
10217
|
-
baseUrl:
|
|
10218
|
-
flagToken:
|
|
10219
|
-
profileName:
|
|
10956
|
+
baseUrl: resolvedCommand.baseUrl,
|
|
10957
|
+
flagToken: resolvedCommand.flagToken,
|
|
10958
|
+
profileName: resolvedCommand.profileName,
|
|
10220
10959
|
env: runtime.env ?? process.env,
|
|
10221
10960
|
});
|
|
10222
10961
|
const { job } = await requestPreviewStatus({
|
|
10223
|
-
command,
|
|
10962
|
+
command: resolvedCommand,
|
|
10224
10963
|
credentials,
|
|
10225
10964
|
fetcher: runtime.fetcher ?? fetch,
|
|
10226
10965
|
});
|
|
10227
|
-
const stream =
|
|
10228
|
-
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' : ''}`, {
|
|
10229
10968
|
headers: {
|
|
10230
10969
|
authorization: `Bearer ${credentials.token}`,
|
|
10231
10970
|
accept: stream ? 'text/event-stream' : 'application/json',
|
|
@@ -10238,7 +10977,7 @@ async function runPreviewWatch(command, io, runtime) {
|
|
|
10238
10977
|
await emitSseLifecycleStream({
|
|
10239
10978
|
response,
|
|
10240
10979
|
io,
|
|
10241
|
-
json:
|
|
10980
|
+
json: resolvedCommand.json,
|
|
10242
10981
|
});
|
|
10243
10982
|
return 0;
|
|
10244
10983
|
}
|
|
@@ -10248,15 +10987,15 @@ async function runPreviewWatch(command, io, runtime) {
|
|
|
10248
10987
|
}
|
|
10249
10988
|
const result = {
|
|
10250
10989
|
ok: true,
|
|
10251
|
-
command:
|
|
10990
|
+
command: resolvedCommand.command,
|
|
10252
10991
|
baseUrl: credentials.baseUrl,
|
|
10253
10992
|
authSource: credentials.source,
|
|
10254
10993
|
profile: credentials.profileName,
|
|
10255
|
-
artifactId:
|
|
10994
|
+
artifactId: resolvedCommand.artifactId,
|
|
10256
10995
|
job: payload.job ?? job,
|
|
10257
10996
|
events: payload.events ?? [],
|
|
10258
10997
|
};
|
|
10259
|
-
if (
|
|
10998
|
+
if (resolvedCommand.json) {
|
|
10260
10999
|
io.stdout(JSON.stringify(result, null, 2));
|
|
10261
11000
|
}
|
|
10262
11001
|
else {
|
|
@@ -10265,38 +11004,39 @@ async function runPreviewWatch(command, io, runtime) {
|
|
|
10265
11004
|
return 0;
|
|
10266
11005
|
}
|
|
10267
11006
|
async function runPublish(command, io, runtime) {
|
|
10268
|
-
|
|
11007
|
+
const resolvedCommand = await resolveCommandCommitSha(command, runtime);
|
|
11008
|
+
if (resolvedCommand.dryRun) {
|
|
10269
11009
|
const result = {
|
|
10270
11010
|
ok: true,
|
|
10271
|
-
command:
|
|
10272
|
-
artifactId:
|
|
10273
|
-
appId:
|
|
10274
|
-
commitSha:
|
|
11011
|
+
command: resolvedCommand.command,
|
|
11012
|
+
artifactId: resolvedCommand.artifactId,
|
|
11013
|
+
appId: resolvedCommand.artifactId,
|
|
11014
|
+
commitSha: resolvedCommand.commitSha,
|
|
10275
11015
|
dryRun: true,
|
|
10276
11016
|
confirmed: false,
|
|
10277
|
-
message:
|
|
11017
|
+
message: resolvedCommand.message,
|
|
10278
11018
|
wouldRequest: {
|
|
10279
11019
|
method: 'POST',
|
|
10280
|
-
path: `/api/v1/artifacts/${
|
|
10281
|
-
stream:
|
|
11020
|
+
path: `/api/v1/artifacts/${resolvedCommand.artifactId}/publish`,
|
|
11021
|
+
stream: resolvedCommand.stream,
|
|
10282
11022
|
body: {
|
|
10283
|
-
...(
|
|
10284
|
-
...(
|
|
11023
|
+
...(resolvedCommand.commitSha ? { commitSha: resolvedCommand.commitSha } : {}),
|
|
11024
|
+
...(resolvedCommand.message ? { message: resolvedCommand.message } : {}),
|
|
10285
11025
|
},
|
|
10286
11026
|
},
|
|
10287
11027
|
};
|
|
10288
|
-
if (
|
|
11028
|
+
if (resolvedCommand.json) {
|
|
10289
11029
|
io.stdout(JSON.stringify(result, null, 2));
|
|
10290
11030
|
}
|
|
10291
11031
|
else {
|
|
10292
|
-
io.stdout([`app_id: ${
|
|
11032
|
+
io.stdout([`app_id: ${resolvedCommand.artifactId}`, 'dry_run: true', 'next: rerun with --confirm publish to publish.'].join('\n'));
|
|
10293
11033
|
}
|
|
10294
11034
|
return 0;
|
|
10295
11035
|
}
|
|
10296
11036
|
return await postLifecycle({
|
|
10297
|
-
command,
|
|
11037
|
+
command: resolvedCommand,
|
|
10298
11038
|
operation: 'publish',
|
|
10299
|
-
path: `/api/v1/artifacts/${encodeURIComponent(
|
|
11039
|
+
path: `/api/v1/artifacts/${encodeURIComponent(resolvedCommand.artifactId)}/publish`,
|
|
10300
11040
|
io,
|
|
10301
11041
|
runtime,
|
|
10302
11042
|
});
|
|
@@ -10966,7 +11706,7 @@ async function resolvePlaybooksCredentials(command, runtime) {
|
|
|
10966
11706
|
if (error instanceof TenderCliUsageError) {
|
|
10967
11707
|
throw new TenderCliUsageError([
|
|
10968
11708
|
'Playbooks require Tender authentication.',
|
|
10969
|
-
'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>.',
|
|
10970
11710
|
].join('\n'));
|
|
10971
11711
|
}
|
|
10972
11712
|
throw error;
|
|
@@ -11070,7 +11810,7 @@ async function resolveConnectorsCredentials(command, runtime) {
|
|
|
11070
11810
|
if (error instanceof TenderCliUsageError) {
|
|
11071
11811
|
throw new TenderCliUsageError([
|
|
11072
11812
|
'Connectors require Tender account authentication.',
|
|
11073
|
-
'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>.',
|
|
11074
11814
|
].join('\n'));
|
|
11075
11815
|
}
|
|
11076
11816
|
throw error;
|
|
@@ -11095,6 +11835,7 @@ async function runConnectorsList(command, io, runtime) {
|
|
|
11095
11835
|
baseUrl: credentials.baseUrl,
|
|
11096
11836
|
token: credentials.token,
|
|
11097
11837
|
fetcher: runtime.fetcher ?? fetch,
|
|
11838
|
+
hostedWorkspaceHint: Boolean((runtime.env ?? process.env).TENDER_RUNTIME_ARTIFACT_ID),
|
|
11098
11839
|
});
|
|
11099
11840
|
const connectors = readConnectorArray(payload);
|
|
11100
11841
|
printConnectorsPayload(command, io, payload, connectors.length > 0
|
|
@@ -11277,6 +12018,13 @@ function uniqueSortedScopes(scopes) {
|
|
|
11277
12018
|
function parseScopeCsv(value) {
|
|
11278
12019
|
return uniqueSortedScopes((value ?? '').split(/[,\s]+/));
|
|
11279
12020
|
}
|
|
12021
|
+
const DEFAULT_SHOPIFY_CONNECTOR_SOURCE_TEMPLATE = 'shopify-pdp-survey';
|
|
12022
|
+
function parseShopifyConnectorSourceTemplate(value) {
|
|
12023
|
+
if (value === 'minimal' || value === 'shopify-pdp-survey') {
|
|
12024
|
+
return value;
|
|
12025
|
+
}
|
|
12026
|
+
throw new TenderCliUsageError('--template must be one of minimal or shopify-pdp-survey.');
|
|
12027
|
+
}
|
|
11280
12028
|
function defaultShopifySourceScopes(connectorScopes, override) {
|
|
11281
12029
|
if (override) {
|
|
11282
12030
|
return parseScopeCsv(override);
|
|
@@ -11284,6 +12032,108 @@ function defaultShopifySourceScopes(connectorScopes, override) {
|
|
|
11284
12032
|
const fromConnector = Array.isArray(connectorScopes) ? connectorScopes.filter((scope) => typeof scope === 'string') : [];
|
|
11285
12033
|
return uniqueSortedScopes(fromConnector);
|
|
11286
12034
|
}
|
|
12035
|
+
function buildShopifyPdpSurveySourceTemplateFiles() {
|
|
12036
|
+
const placeholderClientUrl = 'https://example.invalid/tender-product-survey/release-placeholder/client.js';
|
|
12037
|
+
const placeholderStylesUrl = 'https://example.invalid/tender-product-survey/release-placeholder/styles.css';
|
|
12038
|
+
const extensionToml = `name = "Tender product survey"
|
|
12039
|
+
type = "theme"
|
|
12040
|
+
`;
|
|
12041
|
+
const blockLiquid = `{% liquid
|
|
12042
|
+
assign selected_version = block.settings.widget_version | default: 'production'
|
|
12043
|
+
case selected_version
|
|
12044
|
+
when 'release-placeholder'
|
|
12045
|
+
assign runtime_src = '${placeholderClientUrl}'
|
|
12046
|
+
assign stylesheet_src = '${placeholderStylesUrl}'
|
|
12047
|
+
else
|
|
12048
|
+
assign runtime_src = '${placeholderClientUrl}'
|
|
12049
|
+
assign stylesheet_src = '${placeholderStylesUrl}'
|
|
12050
|
+
endcase
|
|
12051
|
+
%}
|
|
12052
|
+
|
|
12053
|
+
<div
|
|
12054
|
+
{{ block.shopify_attributes }}
|
|
12055
|
+
data-tender-product-survey-block
|
|
12056
|
+
data-widget-version="{{ selected_version | escape }}"
|
|
12057
|
+
>
|
|
12058
|
+
<link rel="preload" href="{{ stylesheet_src | escape }}" as="style">
|
|
12059
|
+
<link rel="stylesheet" href="{{ stylesheet_src | escape }}">
|
|
12060
|
+
<script type="module" src="{{ runtime_src | escape }}"></script>
|
|
12061
|
+
<tender-product-survey
|
|
12062
|
+
data-question="{{ block.settings.question | escape }}"
|
|
12063
|
+
data-helper="{{ block.settings.helper_text | escape }}"
|
|
12064
|
+
data-options="{{ block.settings.options | escape }}"
|
|
12065
|
+
data-property-name="_tender_survey_source"
|
|
12066
|
+
data-product-id="{{ product.id }}"
|
|
12067
|
+
data-product-handle="{{ product.handle | escape }}"
|
|
12068
|
+
></tender-product-survey>
|
|
12069
|
+
</div>
|
|
12070
|
+
|
|
12071
|
+
{% schema %}
|
|
12072
|
+
{
|
|
12073
|
+
"name": "Product survey",
|
|
12074
|
+
"target": "section",
|
|
12075
|
+
"settings": [
|
|
12076
|
+
{
|
|
12077
|
+
"type": "select",
|
|
12078
|
+
"id": "widget_version",
|
|
12079
|
+
"label": "Runtime version",
|
|
12080
|
+
"default": "production",
|
|
12081
|
+
"options": [
|
|
12082
|
+
{
|
|
12083
|
+
"value": "production",
|
|
12084
|
+
"label": "Production pinned release"
|
|
12085
|
+
},
|
|
12086
|
+
{
|
|
12087
|
+
"value": "release-placeholder",
|
|
12088
|
+
"label": "Pinned release placeholder"
|
|
12089
|
+
}
|
|
12090
|
+
]
|
|
12091
|
+
},
|
|
12092
|
+
{
|
|
12093
|
+
"type": "text",
|
|
12094
|
+
"id": "question",
|
|
12095
|
+
"label": "Question",
|
|
12096
|
+
"default": "How did you hear about this product?"
|
|
12097
|
+
},
|
|
12098
|
+
{
|
|
12099
|
+
"type": "text",
|
|
12100
|
+
"id": "helper_text",
|
|
12101
|
+
"label": "Helper text",
|
|
12102
|
+
"default": "Choose one answer and we will save it with your cart."
|
|
12103
|
+
},
|
|
12104
|
+
{
|
|
12105
|
+
"type": "text",
|
|
12106
|
+
"id": "options",
|
|
12107
|
+
"label": "Options",
|
|
12108
|
+
"default": "Instagram|friend|search|other"
|
|
12109
|
+
}
|
|
12110
|
+
]
|
|
12111
|
+
}
|
|
12112
|
+
{% endschema %}
|
|
12113
|
+
`;
|
|
12114
|
+
const readme = `# Tender Product Survey App Block
|
|
12115
|
+
|
|
12116
|
+
This theme app extension contains a thin product page app block for the Tender
|
|
12117
|
+
product survey runtime.
|
|
12118
|
+
|
|
12119
|
+
## Runtime Release Pinning
|
|
12120
|
+
|
|
12121
|
+
Replace the placeholder URLs in \`blocks/product_survey.liquid\` after the
|
|
12122
|
+
runtime is published. Use the exact pinned \`client.js\` and \`styles.css\` URLs
|
|
12123
|
+
from the publish JSON. Keep \`production\` plus recent pinned releases in the
|
|
12124
|
+
\`widget_version\` setting so merchants can switch versions in the theme editor.
|
|
12125
|
+
|
|
12126
|
+
Do not point this block to \`latest\`, and do not expose raw runtime URLs as
|
|
12127
|
+
merchant-editable settings.
|
|
12128
|
+
`;
|
|
12129
|
+
return new Map([
|
|
12130
|
+
['extensions/tender-product-survey/README.md', readme],
|
|
12131
|
+
['extensions/tender-product-survey/package.json', '{\n "name": "tender-product-survey",\n "private": true,\n "version": "0.0.0"\n}\n'],
|
|
12132
|
+
['extensions/tender-product-survey/shopify.extension.toml', extensionToml],
|
|
12133
|
+
['extensions/tender-product-survey/blocks/product_survey.liquid', blockLiquid],
|
|
12134
|
+
['extensions/tender-product-survey/locales/en.default.json', '{}\n'],
|
|
12135
|
+
]);
|
|
12136
|
+
}
|
|
11287
12137
|
function buildShopifySourceFiles(input) {
|
|
11288
12138
|
const appName = requireShopifySourceString(input.connector.appName, 'Tender Prompt Apps');
|
|
11289
12139
|
const adminApiVersion = requireShopifySourceString(input.connector.adminApiVersion, '2026-04');
|
|
@@ -11298,6 +12148,7 @@ function buildShopifySourceFiles(input) {
|
|
|
11298
12148
|
adminApiVersion,
|
|
11299
12149
|
clientId: input.clientId,
|
|
11300
12150
|
scopes,
|
|
12151
|
+
sourceTemplate: input.template,
|
|
11301
12152
|
secrets: {
|
|
11302
12153
|
clientSecret: 'stored in Tender connector credentials, not in this source tree',
|
|
11303
12154
|
appAutomationToken: 'stored in Tender connector secret store, not in this source tree',
|
|
@@ -11323,6 +12174,24 @@ api_version = ${tomlString(adminApiVersion)}
|
|
|
11323
12174
|
[build]
|
|
11324
12175
|
automatically_update_urls_on_dev = true
|
|
11325
12176
|
`;
|
|
12177
|
+
const templateFileSummary = input.template === 'shopify-pdp-survey'
|
|
12178
|
+
? '- `extensions/tender-product-survey/`: A thin product page app block that loads a pinned Tender runtime release.\n'
|
|
12179
|
+
: '';
|
|
12180
|
+
const addCapabilities = input.template === 'shopify-pdp-survey'
|
|
12181
|
+
? 'This bootstrap includes the first product page survey app block only. Add other app blocks, web pixels, customer metadata definitions, or app-owned GraphQL resources as separate source edits guided by Shopify connector playbooks.'
|
|
12182
|
+
: 'This bootstrap intentionally does not generate app blocks, web pixels, customer metadata definitions, or app-owned GraphQL resources. Add those as source edits using the Shopify connector playbooks so the source tree stays the durable record of what the merchant-owned app actually contains.';
|
|
12183
|
+
const runtimeReleasePinning = input.template === 'shopify-pdp-survey'
|
|
12184
|
+
? `## Runtime Release Pin
|
|
12185
|
+
|
|
12186
|
+
The product survey block must load a pinned Tender runtime release. After
|
|
12187
|
+
publishing the survey runtime, replace the placeholder URLs in
|
|
12188
|
+
\`extensions/tender-product-survey/blocks/product_survey.liquid\` with the exact
|
|
12189
|
+
pinned URLs returned by the publish JSON. Keep \`production\` plus recent pinned
|
|
12190
|
+
releases in the \`widget_version\` setting so the merchant can switch versions in
|
|
12191
|
+
the theme editor. Do not point the block at a \`latest\` URL.
|
|
12192
|
+
|
|
12193
|
+
`
|
|
12194
|
+
: '';
|
|
11326
12195
|
const readme = `# ${appName}
|
|
11327
12196
|
|
|
11328
12197
|
This is the merchant-owned Shopify app source bootstrapped by Tender Prompt for
|
|
@@ -11333,13 +12202,13 @@ connector \`${input.connectorId}\`.
|
|
|
11333
12202
|
- \`shopify.app.toml\`: Shopify app configuration and reviewed Admin API scopes.
|
|
11334
12203
|
- \`connector.json\`: Non-secret Tender metadata that maps this source tree back
|
|
11335
12204
|
to the connector.
|
|
12205
|
+
${templateFileSummary}
|
|
11336
12206
|
|
|
11337
12207
|
## Add capabilities
|
|
11338
12208
|
|
|
11339
|
-
|
|
11340
|
-
|
|
11341
|
-
|
|
11342
|
-
record of what the merchant-owned app actually contains.
|
|
12209
|
+
${addCapabilities}
|
|
12210
|
+
|
|
12211
|
+
${runtimeReleasePinning}
|
|
11343
12212
|
|
|
11344
12213
|
## Secrets
|
|
11345
12214
|
|
|
@@ -11359,15 +12228,32 @@ npx --yes @shopify/cli@latest app config validate --path . --json
|
|
|
11359
12228
|
\`\`\`sh
|
|
11360
12229
|
tender connectors shopify source deploy ${input.connectorId} --dir . --confirm deploy --profile account --json
|
|
11361
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.
|
|
11362
12242
|
`;
|
|
12243
|
+
const files = new Map([
|
|
12244
|
+
['.gitignore', '.shopify/\nnode_modules/\n.env\n.env.*\n'],
|
|
12245
|
+
['README.md', readme],
|
|
12246
|
+
['connector.json', `${JSON.stringify(connectorManifest, null, 2)}\n`],
|
|
12247
|
+
['shopify.app.toml', shopifyAppToml],
|
|
12248
|
+
]);
|
|
12249
|
+
if (input.template === 'shopify-pdp-survey') {
|
|
12250
|
+
for (const [relativePath, content] of buildShopifyPdpSurveySourceTemplateFiles()) {
|
|
12251
|
+
files.set(relativePath, content);
|
|
12252
|
+
}
|
|
12253
|
+
}
|
|
11363
12254
|
return {
|
|
11364
12255
|
manifest: connectorManifest,
|
|
11365
|
-
files
|
|
11366
|
-
['.gitignore', '.shopify/\nnode_modules/\n.env\n.env.*\n'],
|
|
11367
|
-
['README.md', readme],
|
|
11368
|
-
['connector.json', `${JSON.stringify(connectorManifest, null, 2)}\n`],
|
|
11369
|
-
['shopify.app.toml', shopifyAppToml],
|
|
11370
|
-
]),
|
|
12256
|
+
files,
|
|
11371
12257
|
};
|
|
11372
12258
|
}
|
|
11373
12259
|
async function fileExists(filePath) {
|
|
@@ -11760,6 +12646,7 @@ async function runConnectorsShopifySourceInit(command, io, runtime) {
|
|
|
11760
12646
|
connector,
|
|
11761
12647
|
clientId: command.clientId,
|
|
11762
12648
|
scopes: command.scopes,
|
|
12649
|
+
template: command.template,
|
|
11763
12650
|
});
|
|
11764
12651
|
const files = await writeShopifySourceFiles({ dir, files: source.files, force: command.force });
|
|
11765
12652
|
const gitCommit = await commitAndPushSourceFiles({
|
|
@@ -11896,6 +12783,264 @@ async function runConnectorsShopifySourceDeploy(command, io, runtime) {
|
|
|
11896
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']);
|
|
11897
12784
|
return 0;
|
|
11898
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
|
+
}
|
|
11899
13044
|
async function runConnectorsShopifyDelete(command, io, runtime) {
|
|
11900
13045
|
if (command.confirm !== 'delete') {
|
|
11901
13046
|
throw new TenderCliUsageError('--confirm delete is required. Example: tender connectors shopify delete conn_123 --confirm delete --json');
|
|
@@ -12804,6 +13949,9 @@ function exampleForError(error, args, command) {
|
|
|
12804
13949
|
if (error instanceof TenderCliDbApiError || error instanceof TenderCliAnalyticsApiError) {
|
|
12805
13950
|
return error.example;
|
|
12806
13951
|
}
|
|
13952
|
+
if (error instanceof TenderCliConnectorsApiError) {
|
|
13953
|
+
return error.example;
|
|
13954
|
+
}
|
|
12807
13955
|
const commandName = commandNameForError(args, command);
|
|
12808
13956
|
const dbPath = dbPathFromCommandName(commandName);
|
|
12809
13957
|
const artifactId = artifactIdForError(args, command) ?? 'artifact_123';
|
|
@@ -12930,6 +14078,14 @@ export async function runTenderCli(args, io = {
|
|
|
12930
14078
|
else if (command.topic === 'connectors bind') {
|
|
12931
14079
|
io.stdout(connectorsBindHelp());
|
|
12932
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
|
+
}
|
|
12933
14089
|
else if (command.topic === 'auth') {
|
|
12934
14090
|
io.stdout(authHelp());
|
|
12935
14091
|
}
|
|
@@ -13190,6 +14346,15 @@ export async function runTenderCli(args, io = {
|
|
|
13190
14346
|
if (command.command === 'connectors shopify source deploy') {
|
|
13191
14347
|
return await runConnectorsShopifySourceDeploy(command, io, runtime);
|
|
13192
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
|
+
}
|
|
13193
14358
|
if (command.command === 'connectors shopify delete') {
|
|
13194
14359
|
return await runConnectorsShopifyDelete(command, io, runtime);
|
|
13195
14360
|
}
|
|
@@ -13362,7 +14527,7 @@ export async function runTenderCli(args, io = {
|
|
|
13362
14527
|
}
|
|
13363
14528
|
catch (error) {
|
|
13364
14529
|
if (args.includes('--json') &&
|
|
13365
|
-
((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')) {
|
|
13366
14531
|
io.stdout(JSON.stringify(cliErrorPayload(error, args, command), null, 2));
|
|
13367
14532
|
return error instanceof TenderCliUsageError ? 2 : 1;
|
|
13368
14533
|
}
|