@tenderprompt/cli 0.1.33 → 0.1.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -5
- package/dist/index.js +1247 -174
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -31,6 +31,9 @@ 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;
|
|
35
|
+
const SHOPIFY_CLI_PACKAGE = '@shopify/cli@latest';
|
|
36
|
+
const SHOPIFY_CLI_BIN = 'shopify';
|
|
34
37
|
const ANALYTICS_AUTHORING_PLAYBOOK_ID = 'analytics-event-authoring';
|
|
35
38
|
const ANALYTICS_AUTHORING_COMMAND = 'tender app analytics authoring --json';
|
|
36
39
|
const ANALYTICS_AUTHORING_PLAYBOOK_COMMAND = `tender playbooks get ${ANALYTICS_AUTHORING_PLAYBOOK_ID} --json`;
|
|
@@ -89,7 +92,7 @@ const ANALYTICS_AGENT_GUIDANCE = {
|
|
|
89
92
|
'clear split between shopper-facing UI and operator-facing analytics',
|
|
90
93
|
],
|
|
91
94
|
};
|
|
92
|
-
const DEFAULT_TENDER_APP_SCAFFOLD_TEMPLATE = '
|
|
95
|
+
const DEFAULT_TENDER_APP_SCAFFOLD_TEMPLATE = 'server-backed';
|
|
93
96
|
class TenderCliUsageError extends Error {
|
|
94
97
|
constructor(message) {
|
|
95
98
|
super(message);
|
|
@@ -134,12 +137,31 @@ class TenderCliConnectorsApiError extends Error {
|
|
|
134
137
|
reasonCode;
|
|
135
138
|
status;
|
|
136
139
|
payload;
|
|
137
|
-
|
|
140
|
+
example;
|
|
141
|
+
constructor(message, reasonCode, status, payload = null, example = 'tender connectors list --profile account --json') {
|
|
138
142
|
super(message);
|
|
139
143
|
this.name = 'TenderCliConnectorsApiError';
|
|
140
144
|
this.reasonCode = reasonCode;
|
|
141
145
|
this.status = status;
|
|
142
146
|
this.payload = payload;
|
|
147
|
+
this.example = example;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
class TenderCliChildProcessError extends Error {
|
|
151
|
+
command;
|
|
152
|
+
args;
|
|
153
|
+
exitCode;
|
|
154
|
+
stdout;
|
|
155
|
+
stderr;
|
|
156
|
+
constructor(input) {
|
|
157
|
+
const detail = input.stderr.trim() || input.stdout.trim();
|
|
158
|
+
super([`${input.command} ${input.args.join(' ')} failed with exit code ${input.exitCode}.`, detail].filter(Boolean).join('\n'));
|
|
159
|
+
this.name = 'TenderCliChildProcessError';
|
|
160
|
+
this.command = input.command;
|
|
161
|
+
this.args = input.args;
|
|
162
|
+
this.exitCode = input.exitCode;
|
|
163
|
+
this.stdout = input.stdout;
|
|
164
|
+
this.stderr = input.stderr;
|
|
143
165
|
}
|
|
144
166
|
}
|
|
145
167
|
const DEFAULT_PROFILE_NAME = 'default';
|
|
@@ -177,6 +199,12 @@ Commands:
|
|
|
177
199
|
tender connectors shopify verify
|
|
178
200
|
Exchange credentials for an Admin API access token
|
|
179
201
|
tender connectors bind Print an app.json binding for a connector instance
|
|
202
|
+
tender shopify workspace connectors list
|
|
203
|
+
List current-store Shopify connectors from a hosted workspace
|
|
204
|
+
tender shopify workspace link-runtime --connector <connector-id>
|
|
205
|
+
Link the hosted runtime to a Shopify connector
|
|
206
|
+
tender shopify workspace deploy-source
|
|
207
|
+
Deploy linked Shopify source from a hosted workspace
|
|
180
208
|
tender app list List Tender App records visible to the current token
|
|
181
209
|
tender app create --name <name>
|
|
182
210
|
Create a new Tender App in the current account
|
|
@@ -239,8 +267,8 @@ Commands:
|
|
|
239
267
|
Examples:
|
|
240
268
|
tender --version
|
|
241
269
|
tender capabilities --json
|
|
242
|
-
tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d
|
|
243
|
-
tender auth status --json
|
|
270
|
+
tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d --save-profile agent --json
|
|
271
|
+
tender auth status --profile agent --json
|
|
244
272
|
tender memory add --kind decision --summary "Kept auth in the host gateway" --json
|
|
245
273
|
tender memory handoff --reason "Preview publish is blocked" --summary "Need Tender support" --json
|
|
246
274
|
tender memory brief --json
|
|
@@ -254,15 +282,18 @@ Examples:
|
|
|
254
282
|
printf "$SHOPIFY_APP_AUTOMATION_TOKEN" | tender connectors shopify app-automation-token set conn_123 --token-stdin --expires-at 2026-08-22 --profile account --json
|
|
255
283
|
tender connectors shopify verify conn_123 --profile account --json
|
|
256
284
|
tender connectors bind SHOPIFY_CONNECTOR conn_123 --instance default --json
|
|
285
|
+
tender shopify workspace connectors list --json
|
|
286
|
+
tender shopify workspace link-runtime --connector conn_123 --json
|
|
287
|
+
tender shopify workspace deploy-source --json
|
|
257
288
|
tender app list --json
|
|
258
289
|
tender app create --name "Exit Intent Widget" --json
|
|
259
|
-
tender app create --name "
|
|
260
|
-
tender app create --name "
|
|
290
|
+
tender app create --name "Data Collection App" --init --dir ./data-collection-app --preview --json
|
|
291
|
+
tender app create --name "Product Page Survey Runtime" --init --dir ./product-survey-runtime --scaffold shopify-pdp-survey-runtime --preview --json
|
|
261
292
|
tender app update artifact_123 --name "Sale Widget" --json
|
|
262
293
|
tender app share create artifact_123 --expires 7d --claims 1 --json
|
|
263
294
|
tender app claim tok_share_123 --dir ./widget --confirm source-and-setup --json
|
|
264
|
-
tender app init artifact_123 --dir ./
|
|
265
|
-
tender app init artifact_123 --dir ./
|
|
295
|
+
tender app init artifact_123 --dir ./data-collection-app --preview --json
|
|
296
|
+
tender app init artifact_123 --dir ./product-survey-runtime --scaffold shopify-pdp-survey-runtime --preview --json
|
|
266
297
|
tender app validate artifact_123 --json
|
|
267
298
|
tender app build artifact_123 --commit $(git rev-parse HEAD) --json
|
|
268
299
|
tender app preview artifact_123 --commit $(git rev-parse HEAD) --stream --json
|
|
@@ -297,12 +328,13 @@ Commands:
|
|
|
297
328
|
|
|
298
329
|
Examples:
|
|
299
330
|
tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d
|
|
300
|
-
tender auth login --device --profile publish --artifact artifact_123 --ttl 7d --save-profile publish
|
|
331
|
+
tender auth login --device --profile publish --artifact artifact_123 --ttl 7d --save-profile publish --json
|
|
332
|
+
tender auth status --profile publish --json
|
|
301
333
|
tender auth login --device --profile create --shopify-store tender-prompt --json
|
|
302
334
|
tender auth status --json`;
|
|
303
335
|
}
|
|
304
336
|
function authLoginHelp() {
|
|
305
|
-
return `Usage: tender auth login --device [--base-url <url>] [--shopify-store <store>] [--shopify-app-url <url>] [--profile <edit-preview|publish|create|db-read>] [--artifact <artifact-id>] [--ttl <7d|14d|30d>] [--save-profile <name>] [--no-poll] [--json]
|
|
337
|
+
return `Usage: tender auth login --device [--base-url <url>] [--shopify-store <store>] [--shopify-app-url <url>] [--profile <edit-preview|publish|create|db-read>] [--artifact <artifact-id>] [--ttl <7d|14d|30d>] [--save-profile <name>] [--no-poll|--poll] [--json]
|
|
306
338
|
|
|
307
339
|
Options:
|
|
308
340
|
--device Use the OAuth device-code flow.
|
|
@@ -313,12 +345,14 @@ Options:
|
|
|
313
345
|
--artifact <artifact-id> Scope the token to one artifact. Required for normal edit-preview agent work.
|
|
314
346
|
--ttl <value> Token lifetime: 7d, 14d, or 30d. Defaults to 7d.
|
|
315
347
|
--save-profile <name> Local config profile name. Defaults to default, or shopify-<store> with Shopify approval.
|
|
316
|
-
--no-poll Print the verification URL and exit without waiting
|
|
317
|
-
--
|
|
348
|
+
--no-poll Print the verification URL, store the pending device auth, and exit without waiting.
|
|
349
|
+
--poll With --json, wait for approval and save the profile after printing the pending handoff to stderr.
|
|
350
|
+
--json Emit stable JSON for coding agents. Defaults to --no-poll unless --poll is provided.
|
|
318
351
|
|
|
319
352
|
Examples:
|
|
320
353
|
tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d
|
|
321
|
-
tender auth login --device --profile publish --artifact artifact_123 --ttl 7d --save-profile publish --json
|
|
354
|
+
tender auth login --device --profile publish --artifact artifact_123 --ttl 7d --save-profile publish --no-poll --json
|
|
355
|
+
tender auth status --profile publish --json
|
|
322
356
|
tender auth login --device --profile create --ttl 7d --save-profile account
|
|
323
357
|
tender auth login --device --profile create --shopify-store tender-prompt --json
|
|
324
358
|
tender auth login --device --profile db-read --artifact artifact_123 --ttl 7d --save-profile db`;
|
|
@@ -463,7 +497,34 @@ Examples:
|
|
|
463
497
|
tender connectors shopify source init conn_123 --dir ./shopify-app --client-id abc --profile account --json
|
|
464
498
|
tender connectors shopify source validate conn_123 --dir ./shopify-app --execute --json
|
|
465
499
|
tender connectors shopify source deploy conn_123 --dir ./shopify-app --confirm deploy --profile account --json
|
|
466
|
-
|
|
500
|
+
tender connectors bind SHOPIFY_CONNECTOR conn_123 --instance default --json`;
|
|
501
|
+
}
|
|
502
|
+
function shopifyWorkspaceHelp() {
|
|
503
|
+
return `Usage: tender shopify workspace <command> [--json]
|
|
504
|
+
|
|
505
|
+
Hosted Shopify workspace commands use the current terminal workspace identity.
|
|
506
|
+
They do not read Tender profiles, account-scoped tokens, Shopify client secrets,
|
|
507
|
+
or Shopify App Automation Tokens from the terminal.
|
|
508
|
+
|
|
509
|
+
Commands:
|
|
510
|
+
tender shopify workspace connectors list
|
|
511
|
+
List current-store Shopify connectors available to this runtime
|
|
512
|
+
tender shopify workspace link-runtime --connector <connector-id>
|
|
513
|
+
Link this runtime to a Shopify connector source project
|
|
514
|
+
tender shopify workspace validate-source
|
|
515
|
+
Validate the linked Shopify connector source
|
|
516
|
+
tender shopify workspace deploy-source
|
|
517
|
+
Deploy the linked Shopify connector source
|
|
518
|
+
|
|
519
|
+
Required environment:
|
|
520
|
+
TENDER_BASE_URL
|
|
521
|
+
TENDER_RUNTIME_ARTIFACT_ID
|
|
522
|
+
|
|
523
|
+
Examples:
|
|
524
|
+
tender shopify workspace connectors list --json
|
|
525
|
+
tender shopify workspace link-runtime --connector conn_123 --json
|
|
526
|
+
tender shopify workspace validate-source --json
|
|
527
|
+
tender shopify workspace deploy-source --json`;
|
|
467
528
|
}
|
|
468
529
|
function connectorsShopifyCreateHelp() {
|
|
469
530
|
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]
|
|
@@ -550,7 +611,7 @@ function connectorsShopifySourceValidateHelp() {
|
|
|
550
611
|
return `Usage: tender connectors shopify source validate <connector-id> [--dir <path>] [--client-id <shopify-client-id>] [--execute] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
551
612
|
|
|
552
613
|
Without --execute, prints the exact Shopify CLI validation command. With
|
|
553
|
-
--execute, runs
|
|
614
|
+
the --execute flag, runs npm exec --yes --package @shopify/cli@latest -- shopify app config validate with npm lifecycle environment variables scrubbed.
|
|
554
615
|
|
|
555
616
|
Examples:
|
|
556
617
|
tender connectors shopify source validate conn_123 --dir ./shopify-app --execute --json`;
|
|
@@ -559,15 +620,17 @@ function connectorsShopifySourceDeployHelp() {
|
|
|
559
620
|
return `Usage: tender connectors shopify source deploy <connector-id> [--dir <path>] [--client-id <shopify-client-id>] [--version <tag>] [--message <text>] [--source-control-url <url>] [--allow-deletes] [--confirm deploy] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
560
621
|
|
|
561
622
|
Without --confirm deploy, prints the exact Shopify CLI deployment command. With
|
|
562
|
-
--confirm deploy, runs
|
|
623
|
+
the --confirm deploy flag, runs npm exec --yes --package @shopify/cli@latest -- shopify app deploy with npm lifecycle environment variables scrubbed. If
|
|
563
624
|
SHOPIFY_APP_AUTOMATION_TOKEN is not set locally, the CLI requests the
|
|
564
625
|
connector's stored App Automation Token and injects it only into the spawned
|
|
565
626
|
Shopify CLI child process.
|
|
566
627
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
628
|
+
Inside hosted Shopify project workspaces, direct shopify app deploy is allowed
|
|
629
|
+
when Shopify CLI is already authenticated there. Otherwise use
|
|
630
|
+
tender shopify workspace deploy-source so Tender can use the connector's
|
|
631
|
+
stored App Automation Token server-side. Do not run this account-scoped command
|
|
632
|
+
with a hosted workspace artifact token. Never ask the merchant to paste token
|
|
633
|
+
material into the terminal.
|
|
571
634
|
|
|
572
635
|
Examples:
|
|
573
636
|
tender connectors shopify source deploy conn_123 --dir ./shopify-app --version connector-source-smoke --confirm deploy --profile account --json`;
|
|
@@ -687,14 +750,14 @@ Commands:
|
|
|
687
750
|
Examples:
|
|
688
751
|
tender app list --json
|
|
689
752
|
tender app create --name "Exit Intent Widget" --json
|
|
690
|
-
tender app create --name "
|
|
691
|
-
tender app create --name "
|
|
753
|
+
tender app create --name "Data Collection App" --init --dir ./data-collection-app --preview --json
|
|
754
|
+
tender app create --name "Product Page Survey Runtime" --init --dir ./product-survey-runtime --scaffold shopify-pdp-survey-runtime --preview --json
|
|
692
755
|
tender app update artifact_123 --name "Sale Widget" --json
|
|
693
756
|
tender app share create artifact_123 --expires 7d --claims 1 --json
|
|
694
757
|
tender app share inspect tok_share_123 --json
|
|
695
758
|
tender app claim tok_share_123 --dir ./widget --confirm source-and-setup --json
|
|
696
|
-
tender app init artifact_123 --dir ./
|
|
697
|
-
tender app init artifact_123 --dir ./
|
|
759
|
+
tender app init artifact_123 --dir ./data-collection-app --preview --json
|
|
760
|
+
tender app init artifact_123 --dir ./product-survey-runtime --scaffold shopify-pdp-survey-runtime --preview --json
|
|
698
761
|
tender app validate artifact_123 --json
|
|
699
762
|
tender app build artifact_123 --commit $(git rev-parse HEAD) --json
|
|
700
763
|
tender app preview artifact_123 --commit $(git rev-parse HEAD) --stream --json
|
|
@@ -1124,7 +1187,7 @@ Options:
|
|
|
1124
1187
|
--name <name> App name.
|
|
1125
1188
|
--init Initialize a local checkout after creating the app.
|
|
1126
1189
|
--dir <path> Local checkout directory for --init. Defaults to current directory.
|
|
1127
|
-
--scaffold <value> API-derived scaffold for empty app source: server-backed, shopify-pdp-survey-runtime, or none. Defaults to
|
|
1190
|
+
--scaffold <value> API-derived scaffold for empty app source: server-backed, shopify-pdp-survey-runtime, or none. Defaults to server-backed.
|
|
1128
1191
|
--no-scaffold Alias for --scaffold none.
|
|
1129
1192
|
--preview After --init writes and pushes initial source, request a preview build.
|
|
1130
1193
|
--remote <name> Git remote name for --init. Defaults to tender.
|
|
@@ -1139,8 +1202,8 @@ Options:
|
|
|
1139
1202
|
|
|
1140
1203
|
Examples:
|
|
1141
1204
|
tender app create --name "Exit Intent Widget" --json
|
|
1142
|
-
tender app create --name "
|
|
1143
|
-
tender app create --name "
|
|
1205
|
+
tender app create --name "Data Collection App" --init --dir ./data-collection-app --preview --json
|
|
1206
|
+
tender app create --name "Product Page Survey Runtime" --init --dir ./product-survey-runtime --scaffold shopify-pdp-survey-runtime --preview --json
|
|
1144
1207
|
tender app create --name "Exit Intent Widget" --profile account --json`;
|
|
1145
1208
|
}
|
|
1146
1209
|
function artifactsUpdateHelp() {
|
|
@@ -1162,7 +1225,7 @@ function artifactsInitHelp() {
|
|
|
1162
1225
|
Options:
|
|
1163
1226
|
--dir <path> Local checkout directory to create/update. Defaults to current directory.
|
|
1164
1227
|
--remote <name> Git remote name to add or update. Defaults to tender.
|
|
1165
|
-
--scaffold <value> API-derived scaffold for empty app source: server-backed, shopify-pdp-survey-runtime, or none. Defaults to
|
|
1228
|
+
--scaffold <value> API-derived scaffold for empty app source: server-backed, shopify-pdp-survey-runtime, or none. Defaults to server-backed.
|
|
1166
1229
|
--no-scaffold Alias for --scaffold none.
|
|
1167
1230
|
--preview Request a preview build for the initialized source commit.
|
|
1168
1231
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
@@ -1175,8 +1238,8 @@ Options:
|
|
|
1175
1238
|
|
|
1176
1239
|
Examples:
|
|
1177
1240
|
tender app init artifact_123 --dir ./widget --dry-run --json
|
|
1178
|
-
tender app init artifact_123 --dir ./
|
|
1179
|
-
tender app init artifact_123 --dir ./
|
|
1241
|
+
tender app init artifact_123 --dir ./data-collection-app --preview --json
|
|
1242
|
+
tender app init artifact_123 --dir ./product-survey-runtime --scaffold shopify-pdp-survey-runtime --json
|
|
1180
1243
|
tender app init artifact_123 --dir ./widget --force --json`;
|
|
1181
1244
|
}
|
|
1182
1245
|
function artifactsContextFetchHelp() {
|
|
@@ -1319,10 +1382,10 @@ Examples:
|
|
|
1319
1382
|
tender app git credential artifact_123 get --remote-url https://app.tenderprompt.com/git/artifacts/artifact_123.git`;
|
|
1320
1383
|
}
|
|
1321
1384
|
function previewRebuildHelp() {
|
|
1322
|
-
return `Usage: tender app preview rebuild <app-id> [--commit <sha>] [--base-url <url>] [--token <token>] [--profile <name>] [--stream] [--json]
|
|
1385
|
+
return `Usage: tender app preview rebuild <app-id> [--commit <sha-or-ref>] [--base-url <url>] [--token <token>] [--profile <name>] [--stream] [--json]
|
|
1323
1386
|
|
|
1324
1387
|
Options:
|
|
1325
|
-
--commit <sha>
|
|
1388
|
+
--commit <sha-or-ref> Rebuild preview only if the app workspace is at this Git commit. Git refs like HEAD are resolved locally.
|
|
1326
1389
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
1327
1390
|
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
1328
1391
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
@@ -1348,10 +1411,10 @@ Examples:
|
|
|
1348
1411
|
tender app validate artifact_123 --stream --json`;
|
|
1349
1412
|
}
|
|
1350
1413
|
function buildHelp() {
|
|
1351
|
-
return `Usage: tender app build <app-id> [--commit <sha>] [--base-url <url>] [--token <token>] [--profile <name>] [--stream] [--json]
|
|
1414
|
+
return `Usage: tender app build <app-id> [--commit <sha-or-ref>] [--base-url <url>] [--token <token>] [--profile <name>] [--stream] [--json]
|
|
1352
1415
|
|
|
1353
1416
|
Options:
|
|
1354
|
-
--commit <sha>
|
|
1417
|
+
--commit <sha-or-ref> Build only if the app workspace is at this Git commit. Git refs like HEAD are resolved locally.
|
|
1355
1418
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
1356
1419
|
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
1357
1420
|
--profile <name> Local config profile name. Defaults to the saved default profile.
|
|
@@ -1363,10 +1426,10 @@ Examples:
|
|
|
1363
1426
|
tender app build artifact_123 --stream --json`;
|
|
1364
1427
|
}
|
|
1365
1428
|
function previewStatusHelp() {
|
|
1366
|
-
return `Usage: tender app preview status <app-id> [--commit <sha> | --job <job-id>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
1429
|
+
return `Usage: tender app preview status <app-id> [--commit <sha-or-ref> | --job <job-id>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
1367
1430
|
|
|
1368
1431
|
Options:
|
|
1369
|
-
--commit <sha>
|
|
1432
|
+
--commit <sha-or-ref> Show the latest preview job for one artifact Git commit. Git refs like HEAD are resolved locally.
|
|
1370
1433
|
--job <job-id> Show one lifecycle job directly.
|
|
1371
1434
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
1372
1435
|
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
@@ -1378,10 +1441,10 @@ Examples:
|
|
|
1378
1441
|
tender app preview status artifact_123 --job job_123 --json`;
|
|
1379
1442
|
}
|
|
1380
1443
|
function previewWatchHelp() {
|
|
1381
|
-
return `Usage: tender app preview watch <app-id> [--commit <sha> | --job <job-id>] [--base-url <url>] [--token <token>] [--profile <name>] [--stream] [--json]
|
|
1444
|
+
return `Usage: tender app preview watch <app-id> [--commit <sha-or-ref> | --job <job-id>] [--base-url <url>] [--token <token>] [--profile <name>] [--stream] [--json]
|
|
1382
1445
|
|
|
1383
1446
|
Options:
|
|
1384
|
-
--commit <sha>
|
|
1447
|
+
--commit <sha-or-ref> Watch the latest preview job for one artifact Git commit. Git refs like HEAD are resolved locally.
|
|
1385
1448
|
--job <job-id> Watch one lifecycle job directly.
|
|
1386
1449
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
1387
1450
|
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
@@ -1394,12 +1457,12 @@ Examples:
|
|
|
1394
1457
|
tender app preview watch artifact_123 --job job_123 --stream --json`;
|
|
1395
1458
|
}
|
|
1396
1459
|
function publishHelp() {
|
|
1397
|
-
return `Usage: tender app publish <app-id> (--dry-run | --confirm publish) [--commit <sha>] [--message <text>] [--base-url <url>] [--token <token>] [--profile <name>] [--stream] [--json]
|
|
1460
|
+
return `Usage: tender app publish <app-id> (--dry-run | --confirm publish) [--commit <sha-or-ref>] [--message <text>] [--base-url <url>] [--token <token>] [--profile <name>] [--stream] [--json]
|
|
1398
1461
|
|
|
1399
1462
|
Options:
|
|
1400
1463
|
--dry-run Show the publish request without calling the API.
|
|
1401
1464
|
--confirm publish Required to call the production-facing publish API.
|
|
1402
|
-
--commit <sha>
|
|
1465
|
+
--commit <sha-or-ref> Publish only if the app workspace is at this Git commit. Git refs like HEAD are resolved locally.
|
|
1403
1466
|
--message <text> Human audit message returned in CLI output.
|
|
1404
1467
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
|
|
1405
1468
|
--token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
|
|
@@ -1592,6 +1655,7 @@ function tenderCliCapabilities() {
|
|
|
1592
1655
|
'tender app outbound-auth --help',
|
|
1593
1656
|
'tender connectors --help',
|
|
1594
1657
|
'tender connectors shopify source init --help',
|
|
1658
|
+
'tender shopify workspace --help',
|
|
1595
1659
|
],
|
|
1596
1660
|
},
|
|
1597
1661
|
workflows: [
|
|
@@ -1600,7 +1664,8 @@ function tenderCliCapabilities() {
|
|
|
1600
1664
|
when: 'Before reading or changing remote Tender App records.',
|
|
1601
1665
|
commands: [
|
|
1602
1666
|
'tender auth status --json',
|
|
1603
|
-
'tender auth login --device --profile edit-preview --artifact <artifact-id> --ttl 7d --json',
|
|
1667
|
+
'tender auth login --device --profile edit-preview --artifact <artifact-id> --ttl 7d --save-profile agent --json',
|
|
1668
|
+
'tender auth status --profile agent --json',
|
|
1604
1669
|
],
|
|
1605
1670
|
},
|
|
1606
1671
|
{
|
|
@@ -1635,16 +1700,46 @@ function tenderCliCapabilities() {
|
|
|
1635
1700
|
'Fetch shopify-connector-customer-metadata-widget when the requested job involves customer metafields, metaobjects, or profile writes.',
|
|
1636
1701
|
],
|
|
1637
1702
|
},
|
|
1703
|
+
{
|
|
1704
|
+
name: 'hosted_shopify_project_release',
|
|
1705
|
+
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.',
|
|
1706
|
+
commands: [
|
|
1707
|
+
'tender shopify workspace connectors list --json',
|
|
1708
|
+
'tender shopify workspace link-runtime --connector <connector-id> --json',
|
|
1709
|
+
'cd /workspace/runtime-project && git push origin HEAD:main',
|
|
1710
|
+
'cd /workspace/runtime-project && commit=$(git rev-parse HEAD) && tender app publish "$TENDER_RUNTIME_ARTIFACT_ID" --commit "$commit" --confirm publish --stream --json',
|
|
1711
|
+
'cd /workspace/shopify-connector && git push origin HEAD:main',
|
|
1712
|
+
'cd /workspace/shopify-connector && tender shopify workspace validate-source --json',
|
|
1713
|
+
'cd /workspace/shopify-connector && tender shopify workspace deploy-source --json',
|
|
1714
|
+
],
|
|
1715
|
+
notes: [
|
|
1716
|
+
'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.',
|
|
1717
|
+
'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.',
|
|
1718
|
+
'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.',
|
|
1719
|
+
'After link-runtime succeeds, tender shopify workspace validate-source and deploy-source automatically use that selected connector.',
|
|
1720
|
+
'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.',
|
|
1721
|
+
'For a validate-only connector request, run cd /workspace/shopify-connector && tender shopify workspace validate-source --json.',
|
|
1722
|
+
'Only run runtime-project checks when the merchant asks to change, validate, preview, or publish the runtime app.',
|
|
1723
|
+
'Publish the Tender runtime first and use the pinned runtime URL from publish JSON to update the linked connector source before deploying Shopify.',
|
|
1724
|
+
'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.',
|
|
1725
|
+
'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.',
|
|
1726
|
+
'Use tender shopify workspace validate-source and deploy-source so the connector stored App Automation Token stays server-side.',
|
|
1727
|
+
'If Shopify CLI is already authenticated in the hosted workspace, direct shopify app config validate and shopify app deploy are allowed.',
|
|
1728
|
+
'Never ask for Shopify App Automation Tokens, client secrets, Tender tokens, or provider keys in the terminal.',
|
|
1729
|
+
'Do not release by pushing Git tags; Tender runtime publish plus tender shopify workspace deploy-source are the release path.',
|
|
1730
|
+
'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.',
|
|
1731
|
+
],
|
|
1732
|
+
},
|
|
1638
1733
|
{
|
|
1639
1734
|
name: 'create_new_app_with_preview',
|
|
1640
|
-
when: 'When the user wants a new
|
|
1735
|
+
when: 'When the user wants a new Tender App that is immediately visible.',
|
|
1641
1736
|
commands: [
|
|
1642
|
-
'tender app create --name "
|
|
1643
|
-
'tender app create --name
|
|
1737
|
+
'tender app create --name "Data Collection App" --init --dir ./data-collection-app --preview --json',
|
|
1738
|
+
'tender app create --name "Product Page Survey Runtime" --init --dir ./product-survey-runtime --scaffold shopify-pdp-survey-runtime --preview --json',
|
|
1644
1739
|
],
|
|
1645
1740
|
notes: [
|
|
1646
|
-
'create --init defaults to the
|
|
1647
|
-
'Pass --scaffold
|
|
1741
|
+
'create --init defaults to the server-backed scaffold when the new app has no source.',
|
|
1742
|
+
'Pass --scaffold shopify-pdp-survey-runtime only for the Shopify product page survey starter.',
|
|
1648
1743
|
'--preview requests a preview build for the initial source commit after the push.',
|
|
1649
1744
|
],
|
|
1650
1745
|
},
|
|
@@ -1739,6 +1834,7 @@ function tenderCliCapabilities() {
|
|
|
1739
1834
|
when: 'When inspecting app-local database tables from a scoped token.',
|
|
1740
1835
|
commands: [
|
|
1741
1836
|
'tender auth login --device --profile db-read --artifact <artifact-id> --ttl 7d --save-profile db --json',
|
|
1837
|
+
'tender auth status --profile db --json',
|
|
1742
1838
|
'tender app db capabilities <artifact-id> --profile db --json',
|
|
1743
1839
|
'tender app db tables <artifact-id> --env published --profile db --json',
|
|
1744
1840
|
'tender app db schema <artifact-id> --env published --table <table-name> --profile db --json',
|
|
@@ -1807,7 +1903,7 @@ function tenderCliCapabilities() {
|
|
|
1807
1903
|
},
|
|
1808
1904
|
{
|
|
1809
1905
|
command: 'tender app create --name <name> --init --dir <dir> --preview --json',
|
|
1810
|
-
purpose: 'Create a new Tender App, initialize the default
|
|
1906
|
+
purpose: 'Create a new Tender App, initialize the default server-backed source in a local checkout, push it, and request a preview build.',
|
|
1811
1907
|
requiresAuth: true,
|
|
1812
1908
|
writes: true,
|
|
1813
1909
|
dryRun: false,
|
|
@@ -2058,6 +2154,45 @@ function normalizeStoredProfile(value) {
|
|
|
2058
2154
|
...normalizeStoredShopifyContext(record),
|
|
2059
2155
|
};
|
|
2060
2156
|
}
|
|
2157
|
+
function normalizeStoredPendingDeviceAuthorization(value) {
|
|
2158
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
2159
|
+
return null;
|
|
2160
|
+
}
|
|
2161
|
+
const record = value;
|
|
2162
|
+
if (typeof record.baseUrl !== 'string' ||
|
|
2163
|
+
typeof record.deviceCode !== 'string' ||
|
|
2164
|
+
typeof record.userCode !== 'string' ||
|
|
2165
|
+
typeof record.verificationUri !== 'string' ||
|
|
2166
|
+
typeof record.verificationUriComplete !== 'string' ||
|
|
2167
|
+
typeof record.expiresAt !== 'number' ||
|
|
2168
|
+
typeof record.tokenProfile !== 'string' ||
|
|
2169
|
+
typeof record.ttl !== 'string') {
|
|
2170
|
+
return null;
|
|
2171
|
+
}
|
|
2172
|
+
let tokenProfile;
|
|
2173
|
+
let ttl;
|
|
2174
|
+
try {
|
|
2175
|
+
tokenProfile = parseTokenProfile(record.tokenProfile);
|
|
2176
|
+
ttl = parseTtl(record.ttl);
|
|
2177
|
+
}
|
|
2178
|
+
catch {
|
|
2179
|
+
return null;
|
|
2180
|
+
}
|
|
2181
|
+
const artifactId = typeof record.artifactId === 'string' || record.artifactId === null ? record.artifactId : undefined;
|
|
2182
|
+
return {
|
|
2183
|
+
baseUrl: normalizeBaseUrl(record.baseUrl),
|
|
2184
|
+
deviceCode: record.deviceCode,
|
|
2185
|
+
userCode: record.userCode,
|
|
2186
|
+
verificationUri: record.verificationUri,
|
|
2187
|
+
verificationUriComplete: record.verificationUriComplete,
|
|
2188
|
+
expiresAt: record.expiresAt,
|
|
2189
|
+
interval: typeof record.interval === 'number' ? record.interval : undefined,
|
|
2190
|
+
tokenProfile,
|
|
2191
|
+
artifactId,
|
|
2192
|
+
ttl,
|
|
2193
|
+
...normalizeStoredShopifyContext(record),
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2061
2196
|
function normalizeStoredShopifyContext(record) {
|
|
2062
2197
|
const shopifyAppUrl = typeof record.shopifyAppUrl === 'string' ? record.shopifyAppUrl : null;
|
|
2063
2198
|
if (shopifyAppUrl) {
|
|
@@ -2101,9 +2236,21 @@ function normalizeConfig(value) {
|
|
|
2101
2236
|
}
|
|
2102
2237
|
}
|
|
2103
2238
|
}
|
|
2239
|
+
const pendingDeviceAuthorizations = {};
|
|
2240
|
+
if (record.pendingDeviceAuthorizations &&
|
|
2241
|
+
typeof record.pendingDeviceAuthorizations === 'object' &&
|
|
2242
|
+
!Array.isArray(record.pendingDeviceAuthorizations)) {
|
|
2243
|
+
for (const [name, pendingAuthorization] of Object.entries(record.pendingDeviceAuthorizations)) {
|
|
2244
|
+
const normalized = normalizeStoredPendingDeviceAuthorization(pendingAuthorization);
|
|
2245
|
+
if (normalized) {
|
|
2246
|
+
pendingDeviceAuthorizations[name] = normalized;
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2104
2250
|
return {
|
|
2105
2251
|
defaultProfile: typeof record.defaultProfile === 'string' && record.defaultProfile ? record.defaultProfile : undefined,
|
|
2106
2252
|
profiles: Object.keys(profiles).length > 0 ? profiles : undefined,
|
|
2253
|
+
pendingDeviceAuthorizations: Object.keys(pendingDeviceAuthorizations).length > 0 ? pendingDeviceAuthorizations : undefined,
|
|
2107
2254
|
};
|
|
2108
2255
|
}
|
|
2109
2256
|
async function readConfig(env) {
|
|
@@ -2170,6 +2317,30 @@ async function resolveApiCredentials(input) {
|
|
|
2170
2317
|
profileName,
|
|
2171
2318
|
};
|
|
2172
2319
|
}
|
|
2320
|
+
async function resolveLifecycleApiCredentials(input) {
|
|
2321
|
+
const baseUrl = normalizeBaseUrl(input.baseUrl ?? input.env.TENDER_BASE_URL ?? DEFAULT_BASE_URL);
|
|
2322
|
+
if (!input.flagToken &&
|
|
2323
|
+
!input.profileName &&
|
|
2324
|
+
isHostedWorkspaceInternalApiBaseUrl(baseUrl)) {
|
|
2325
|
+
return {
|
|
2326
|
+
baseUrl,
|
|
2327
|
+
token: null,
|
|
2328
|
+
source: 'hosted-workspace',
|
|
2329
|
+
profileName: null,
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
return await resolveApiCredentials(input);
|
|
2333
|
+
}
|
|
2334
|
+
function apiHeadersForCredentials(input) {
|
|
2335
|
+
const headers = {
|
|
2336
|
+
accept: input.accept ?? 'application/json',
|
|
2337
|
+
...(input.jsonBody ? { 'content-type': 'application/json' } : {}),
|
|
2338
|
+
};
|
|
2339
|
+
if (input.credentials.token) {
|
|
2340
|
+
headers.authorization = `Bearer ${input.credentials.token}`;
|
|
2341
|
+
}
|
|
2342
|
+
return headers;
|
|
2343
|
+
}
|
|
2173
2344
|
function parseProfileName(value, flag) {
|
|
2174
2345
|
if (!value || value.startsWith('-')) {
|
|
2175
2346
|
throw new TenderCliUsageError(`${flag} requires a profile name.`);
|
|
@@ -2560,7 +2731,7 @@ function parseBuildArgs(args) {
|
|
|
2560
2731
|
if (arg === '--commit') {
|
|
2561
2732
|
const value = args[index + 1];
|
|
2562
2733
|
if (!value || value.startsWith('-')) {
|
|
2563
|
-
throw new TenderCliUsageError('--commit requires a Git commit SHA.');
|
|
2734
|
+
throw new TenderCliUsageError('--commit requires a Git commit SHA or Git ref.');
|
|
2564
2735
|
}
|
|
2565
2736
|
commitSha = value;
|
|
2566
2737
|
index += 1;
|
|
@@ -2621,7 +2792,7 @@ function parsePreviewRebuildArgs(args) {
|
|
|
2621
2792
|
if (arg === '--commit') {
|
|
2622
2793
|
const value = args[index + 1];
|
|
2623
2794
|
if (!value || value.startsWith('-')) {
|
|
2624
|
-
throw new TenderCliUsageError('--commit requires a Git commit SHA.');
|
|
2795
|
+
throw new TenderCliUsageError('--commit requires a Git commit SHA or Git ref.');
|
|
2625
2796
|
}
|
|
2626
2797
|
commitSha = value;
|
|
2627
2798
|
index += 1;
|
|
@@ -2676,7 +2847,7 @@ function readPreviewLookupOptions(input) {
|
|
|
2676
2847
|
if (arg === '--commit') {
|
|
2677
2848
|
const value = input.args[index + 1];
|
|
2678
2849
|
if (!value || value.startsWith('-')) {
|
|
2679
|
-
throw new TenderCliUsageError('--commit requires a commit SHA.');
|
|
2850
|
+
throw new TenderCliUsageError('--commit requires a Git commit SHA or Git ref.');
|
|
2680
2851
|
}
|
|
2681
2852
|
commitSha = value;
|
|
2682
2853
|
index += 1;
|
|
@@ -2825,7 +2996,7 @@ function parsePublishArgs(args) {
|
|
|
2825
2996
|
if (arg === '--commit') {
|
|
2826
2997
|
const value = args[index + 1];
|
|
2827
2998
|
if (!value || value.startsWith('-')) {
|
|
2828
|
-
throw new TenderCliUsageError('--commit requires a commit SHA.');
|
|
2999
|
+
throw new TenderCliUsageError('--commit requires a Git commit SHA or Git ref.');
|
|
2829
3000
|
}
|
|
2830
3001
|
commitSha = value;
|
|
2831
3002
|
index += 1;
|
|
@@ -3320,7 +3491,7 @@ function parseArtifactsCreateArgs(args) {
|
|
|
3320
3491
|
}
|
|
3321
3492
|
if (!init) {
|
|
3322
3493
|
if (preview) {
|
|
3323
|
-
throw new TenderCliUsageError('--preview requires --init. Example: tender app create --name "
|
|
3494
|
+
throw new TenderCliUsageError('--preview requires --init. Example: tender app create --name "Data Collection App" --init --dir ./data-collection-app --preview --json');
|
|
3324
3495
|
}
|
|
3325
3496
|
if (force) {
|
|
3326
3497
|
throw new TenderCliUsageError('--force is only valid with --init.');
|
|
@@ -3867,6 +4038,58 @@ function parseConnectorsShopifySourceDeployArgs(args) {
|
|
|
3867
4038
|
}
|
|
3868
4039
|
return { command: 'connectors shopify source deploy', connectorId, dir, clientId, version, message, sourceControlUrl, allowDeletes, confirm, ...parsed };
|
|
3869
4040
|
}
|
|
4041
|
+
function parseShopifyWorkspaceSourceArgs(args, command) {
|
|
4042
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
4043
|
+
return { command: 'help', topic: command };
|
|
4044
|
+
}
|
|
4045
|
+
let json = false;
|
|
4046
|
+
for (const arg of args) {
|
|
4047
|
+
if (arg === '--json') {
|
|
4048
|
+
json = true;
|
|
4049
|
+
continue;
|
|
4050
|
+
}
|
|
4051
|
+
throw new TenderCliUsageError(`Unknown ${command} option: ${arg}`);
|
|
4052
|
+
}
|
|
4053
|
+
return { command, json };
|
|
4054
|
+
}
|
|
4055
|
+
function parseShopifyWorkspaceConnectorsListArgs(args) {
|
|
4056
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
4057
|
+
return { command: 'help', topic: 'shopify workspace connectors list' };
|
|
4058
|
+
}
|
|
4059
|
+
let json = false;
|
|
4060
|
+
for (const arg of args) {
|
|
4061
|
+
if (arg === '--json') {
|
|
4062
|
+
json = true;
|
|
4063
|
+
continue;
|
|
4064
|
+
}
|
|
4065
|
+
throw new TenderCliUsageError(`Unknown shopify workspace connectors list option: ${arg}`);
|
|
4066
|
+
}
|
|
4067
|
+
return { command: 'shopify workspace connectors list', json };
|
|
4068
|
+
}
|
|
4069
|
+
function parseShopifyWorkspaceLinkRuntimeArgs(args) {
|
|
4070
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
4071
|
+
return { command: 'help', topic: 'shopify workspace link-runtime' };
|
|
4072
|
+
}
|
|
4073
|
+
let connectorId = null;
|
|
4074
|
+
let json = false;
|
|
4075
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
4076
|
+
const arg = args[index];
|
|
4077
|
+
if (arg === '--json') {
|
|
4078
|
+
json = true;
|
|
4079
|
+
continue;
|
|
4080
|
+
}
|
|
4081
|
+
if (arg === '--connector') {
|
|
4082
|
+
connectorId = parseConnectorId(parseRequiredFlagValue(args[index + 1], '--connector'), 'tender shopify workspace link-runtime --connector conn_123 --json');
|
|
4083
|
+
index += 1;
|
|
4084
|
+
continue;
|
|
4085
|
+
}
|
|
4086
|
+
throw new TenderCliUsageError(`Unknown shopify workspace link-runtime option: ${arg}`);
|
|
4087
|
+
}
|
|
4088
|
+
if (!connectorId) {
|
|
4089
|
+
throw new TenderCliUsageError('--connector is required. Example: tender shopify workspace link-runtime --connector conn_123 --json');
|
|
4090
|
+
}
|
|
4091
|
+
return { command: 'shopify workspace link-runtime', connectorId, json };
|
|
4092
|
+
}
|
|
3870
4093
|
function parseConnectorsShopifyDeleteArgs(args) {
|
|
3871
4094
|
if (args.includes('--help') || args.includes('-h')) {
|
|
3872
4095
|
return { command: 'help', topic: 'connectors shopify delete' };
|
|
@@ -4522,6 +4745,7 @@ function parseAuthLoginArgs(args) {
|
|
|
4522
4745
|
let ttl = '7d';
|
|
4523
4746
|
let json = false;
|
|
4524
4747
|
let noPoll = false;
|
|
4748
|
+
let poll = false;
|
|
4525
4749
|
let pollTimeoutSeconds = 600;
|
|
4526
4750
|
for (let index = 0; index < args.length; index += 1) {
|
|
4527
4751
|
const arg = args[index];
|
|
@@ -4534,9 +4758,19 @@ function parseAuthLoginArgs(args) {
|
|
|
4534
4758
|
continue;
|
|
4535
4759
|
}
|
|
4536
4760
|
if (arg === '--no-poll') {
|
|
4761
|
+
if (poll) {
|
|
4762
|
+
throw new TenderCliUsageError('--no-poll cannot be combined with --poll.');
|
|
4763
|
+
}
|
|
4537
4764
|
noPoll = true;
|
|
4538
4765
|
continue;
|
|
4539
4766
|
}
|
|
4767
|
+
if (arg === '--poll') {
|
|
4768
|
+
if (noPoll) {
|
|
4769
|
+
throw new TenderCliUsageError('--poll cannot be combined with --no-poll.');
|
|
4770
|
+
}
|
|
4771
|
+
poll = true;
|
|
4772
|
+
continue;
|
|
4773
|
+
}
|
|
4540
4774
|
if (arg === '--base-url') {
|
|
4541
4775
|
baseUrl = parseBaseUrlFlag(args, index);
|
|
4542
4776
|
index += 1;
|
|
@@ -4583,6 +4817,10 @@ function parseAuthLoginArgs(args) {
|
|
|
4583
4817
|
continue;
|
|
4584
4818
|
}
|
|
4585
4819
|
if (arg === '--poll-timeout') {
|
|
4820
|
+
if (noPoll) {
|
|
4821
|
+
throw new TenderCliUsageError('--poll-timeout cannot be combined with --no-poll.');
|
|
4822
|
+
}
|
|
4823
|
+
poll = true;
|
|
4586
4824
|
pollTimeoutSeconds = parsePositiveIntegerFlag(args[index + 1], '--poll-timeout');
|
|
4587
4825
|
index += 1;
|
|
4588
4826
|
continue;
|
|
@@ -4612,7 +4850,7 @@ function parseAuthLoginArgs(args) {
|
|
|
4612
4850
|
artifactId,
|
|
4613
4851
|
ttl,
|
|
4614
4852
|
json,
|
|
4615
|
-
noPoll,
|
|
4853
|
+
noPoll: noPoll || (json && !poll),
|
|
4616
4854
|
pollTimeoutSeconds,
|
|
4617
4855
|
};
|
|
4618
4856
|
}
|
|
@@ -6947,6 +7185,30 @@ function parseTenderArgs(args) {
|
|
|
6947
7185
|
}
|
|
6948
7186
|
throw new TenderCliUsageError(`Unknown connectors command: ${args[1]}`);
|
|
6949
7187
|
}
|
|
7188
|
+
if (args[0] === 'shopify') {
|
|
7189
|
+
if (args[1] === undefined || args[1] === '--help' || args[1] === '-h') {
|
|
7190
|
+
return { command: 'help', topic: 'shopify' };
|
|
7191
|
+
}
|
|
7192
|
+
if (args[1] === 'workspace') {
|
|
7193
|
+
if (args[2] === undefined || args[2] === '--help' || args[2] === '-h') {
|
|
7194
|
+
return { command: 'help', topic: 'shopify workspace' };
|
|
7195
|
+
}
|
|
7196
|
+
if (args[2] === 'connectors' && args[3] === 'list') {
|
|
7197
|
+
return parseShopifyWorkspaceConnectorsListArgs(args.slice(4));
|
|
7198
|
+
}
|
|
7199
|
+
if (args[2] === 'link-runtime') {
|
|
7200
|
+
return parseShopifyWorkspaceLinkRuntimeArgs(args.slice(3));
|
|
7201
|
+
}
|
|
7202
|
+
if (args[2] === 'validate-source') {
|
|
7203
|
+
return parseShopifyWorkspaceSourceArgs(args.slice(3), 'shopify workspace validate-source');
|
|
7204
|
+
}
|
|
7205
|
+
if (args[2] === 'deploy-source') {
|
|
7206
|
+
return parseShopifyWorkspaceSourceArgs(args.slice(3), 'shopify workspace deploy-source');
|
|
7207
|
+
}
|
|
7208
|
+
throw new TenderCliUsageError(`Unknown shopify workspace command: ${args[2]}`);
|
|
7209
|
+
}
|
|
7210
|
+
throw new TenderCliUsageError(`Unknown shopify command: ${args[1]}`);
|
|
7211
|
+
}
|
|
6950
7212
|
if (args[0] === 'apps') {
|
|
6951
7213
|
throw new TenderCliUsageError('Unknown command: apps. Use `tender app ...` for Tender App workflows.');
|
|
6952
7214
|
}
|
|
@@ -7657,7 +7919,7 @@ async function requestPlaybooksApi(input) {
|
|
|
7657
7919
|
(typeof payload?.reasonCode === 'string' && payload.reasonCode) ||
|
|
7658
7920
|
`Playbooks request failed with HTTP ${response.status}.`;
|
|
7659
7921
|
const authHint = response.status === 401
|
|
7660
|
-
? '\nAuthenticate first: tender auth login --device --profile edit-preview --save-profile agent --ttl 7d --json'
|
|
7922
|
+
? '\nAuthenticate first: tender auth login --device --profile edit-preview --save-profile agent --ttl 7d --json, approve the URL, then run tender auth status --profile agent --json.'
|
|
7661
7923
|
: '';
|
|
7662
7924
|
throw new Error([message, `Reason: ${reasonCode}.`, 'Example: tender playbooks list --profile agent --json.'].join('\n') + authHint);
|
|
7663
7925
|
}
|
|
@@ -7665,13 +7927,16 @@ async function requestPlaybooksApi(input) {
|
|
|
7665
7927
|
}
|
|
7666
7928
|
async function requestConnectorsApi(input) {
|
|
7667
7929
|
const suffix = input.connectorsPath ? `/${input.connectorsPath.replace(/^\/+/, '')}` : '';
|
|
7930
|
+
const headers = {
|
|
7931
|
+
accept: 'application/json',
|
|
7932
|
+
...(input.body === undefined ? {} : { 'content-type': 'application/json' }),
|
|
7933
|
+
};
|
|
7934
|
+
if (input.token) {
|
|
7935
|
+
headers.authorization = `Bearer ${input.token}`;
|
|
7936
|
+
}
|
|
7668
7937
|
const response = await input.fetcher(`${input.baseUrl}/api/account/connectors${suffix}`, {
|
|
7669
7938
|
method: input.method,
|
|
7670
|
-
headers
|
|
7671
|
-
authorization: `Bearer ${input.token}`,
|
|
7672
|
-
accept: 'application/json',
|
|
7673
|
-
...(input.body === undefined ? {} : { 'content-type': 'application/json' }),
|
|
7674
|
-
},
|
|
7939
|
+
headers,
|
|
7675
7940
|
body: input.body === undefined ? undefined : JSON.stringify(input.body),
|
|
7676
7941
|
});
|
|
7677
7942
|
const payload = response.status === 204 ? { ok: true } : await parseJsonResponse(response);
|
|
@@ -7680,10 +7945,189 @@ async function requestConnectorsApi(input) {
|
|
|
7680
7945
|
const message = (typeof payload?.message === 'string' && payload.message) ||
|
|
7681
7946
|
(typeof payload?.reasonCode === 'string' && payload.reasonCode) ||
|
|
7682
7947
|
`Connector request failed with HTTP ${response.status}.`;
|
|
7683
|
-
|
|
7948
|
+
const example = input.hostedWorkspaceHint && reasonCode === 'account_scope_required'
|
|
7949
|
+
? 'Hosted workspace example: tender shopify workspace connectors list --json.'
|
|
7950
|
+
: 'Example: tender connectors list --profile account --json.';
|
|
7951
|
+
throw new TenderCliConnectorsApiError([message, `Reason: ${reasonCode}.`, example].join('\n'), reasonCode, response.status, payload ?? null, example.replace(/^Example:\s*/i, '').replace(/^Hosted workspace example:\s*/i, '').replace(/\.$/, ''));
|
|
7684
7952
|
}
|
|
7685
7953
|
return payload;
|
|
7686
7954
|
}
|
|
7955
|
+
function normalizeHostedWorkspaceBaseUrl(value) {
|
|
7956
|
+
const baseUrl = value?.trim().replace(/\/+$/, '') ?? '';
|
|
7957
|
+
if (!baseUrl) {
|
|
7958
|
+
return null;
|
|
7959
|
+
}
|
|
7960
|
+
try {
|
|
7961
|
+
const url = new URL(baseUrl);
|
|
7962
|
+
return url.protocol === 'https:' || url.protocol === 'http:' ? url.toString().replace(/\/+$/, '') : null;
|
|
7963
|
+
}
|
|
7964
|
+
catch {
|
|
7965
|
+
return null;
|
|
7966
|
+
}
|
|
7967
|
+
}
|
|
7968
|
+
function readHostedWorkspaceArtifactId(value) {
|
|
7969
|
+
const artifactId = value?.trim() ?? '';
|
|
7970
|
+
return /^artifact_[A-Za-z0-9_-]+$/.test(artifactId) ? artifactId : null;
|
|
7971
|
+
}
|
|
7972
|
+
function isHostedWorkspaceInternalApiBaseUrl(baseUrl) {
|
|
7973
|
+
try {
|
|
7974
|
+
return new URL(baseUrl).hostname === 'tender-api.tender.test';
|
|
7975
|
+
}
|
|
7976
|
+
catch {
|
|
7977
|
+
return false;
|
|
7978
|
+
}
|
|
7979
|
+
}
|
|
7980
|
+
function hostedWorkspaceApiHeaders(input) {
|
|
7981
|
+
return {
|
|
7982
|
+
accept: 'application/json',
|
|
7983
|
+
...(input.jsonBody ? { 'content-type': 'application/json' } : {}),
|
|
7984
|
+
};
|
|
7985
|
+
}
|
|
7986
|
+
function readHostedWorkspaceConnectorId(value) {
|
|
7987
|
+
const connectorId = value?.trim() ?? '';
|
|
7988
|
+
return /^conn_[A-Za-z0-9_-]+$/.test(connectorId) ? connectorId : null;
|
|
7989
|
+
}
|
|
7990
|
+
function errorCodeMatches(error, code) {
|
|
7991
|
+
return Boolean(error &&
|
|
7992
|
+
typeof error === 'object' &&
|
|
7993
|
+
'code' in error &&
|
|
7994
|
+
error.code === code);
|
|
7995
|
+
}
|
|
7996
|
+
function hostedWorkspaceSelectionFilePath(runtime) {
|
|
7997
|
+
const env = runtime.env ?? process.env;
|
|
7998
|
+
const explicitPath = env.TENDER_WORKSPACE_STATE_FILE?.trim();
|
|
7999
|
+
if (explicitPath) {
|
|
8000
|
+
return path.resolve(explicitPath);
|
|
8001
|
+
}
|
|
8002
|
+
const runtimeProjectDir = env.TENDER_RUNTIME_PROJECT_DIR?.trim();
|
|
8003
|
+
if (runtimeProjectDir && path.isAbsolute(runtimeProjectDir)) {
|
|
8004
|
+
return path.join(path.dirname(runtimeProjectDir), '.tender', 'shopify-workspace.json');
|
|
8005
|
+
}
|
|
8006
|
+
const cwd = process.cwd();
|
|
8007
|
+
if (cwd === '/workspace' || cwd.startsWith('/workspace/')) {
|
|
8008
|
+
return '/workspace/.tender/shopify-workspace.json';
|
|
8009
|
+
}
|
|
8010
|
+
return null;
|
|
8011
|
+
}
|
|
8012
|
+
async function writeHostedWorkspaceConnectorSelection(input, runtime) {
|
|
8013
|
+
const filePath = hostedWorkspaceSelectionFilePath(runtime);
|
|
8014
|
+
if (!filePath) {
|
|
8015
|
+
return;
|
|
8016
|
+
}
|
|
8017
|
+
await mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
8018
|
+
await writeFile(filePath, `${JSON.stringify({
|
|
8019
|
+
version: 1,
|
|
8020
|
+
runtimeArtifactId: input.runtimeArtifactId,
|
|
8021
|
+
connectorId: input.connectorId,
|
|
8022
|
+
selectedAt: Date.now(),
|
|
8023
|
+
}, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
8024
|
+
}
|
|
8025
|
+
async function readHostedWorkspaceConnectorSelection(runtimeArtifactId, runtime) {
|
|
8026
|
+
const env = runtime.env ?? process.env;
|
|
8027
|
+
const envConnectorId = readHostedWorkspaceConnectorId(env.TENDER_SHOPIFY_CONNECTOR_ID);
|
|
8028
|
+
if (envConnectorId) {
|
|
8029
|
+
return envConnectorId;
|
|
8030
|
+
}
|
|
8031
|
+
const filePath = hostedWorkspaceSelectionFilePath(runtime);
|
|
8032
|
+
if (!filePath) {
|
|
8033
|
+
return null;
|
|
8034
|
+
}
|
|
8035
|
+
let content;
|
|
8036
|
+
try {
|
|
8037
|
+
content = await readFile(filePath, 'utf8');
|
|
8038
|
+
}
|
|
8039
|
+
catch (error) {
|
|
8040
|
+
if (errorCodeMatches(error, 'ENOENT')) {
|
|
8041
|
+
return null;
|
|
8042
|
+
}
|
|
8043
|
+
throw error;
|
|
8044
|
+
}
|
|
8045
|
+
let parsed;
|
|
8046
|
+
try {
|
|
8047
|
+
parsed = JSON.parse(content);
|
|
8048
|
+
}
|
|
8049
|
+
catch {
|
|
8050
|
+
return null;
|
|
8051
|
+
}
|
|
8052
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
8053
|
+
return null;
|
|
8054
|
+
}
|
|
8055
|
+
const record = parsed;
|
|
8056
|
+
if (record.runtimeArtifactId !== runtimeArtifactId) {
|
|
8057
|
+
return null;
|
|
8058
|
+
}
|
|
8059
|
+
return readHostedWorkspaceConnectorId(typeof record.connectorId === 'string' ? record.connectorId : undefined);
|
|
8060
|
+
}
|
|
8061
|
+
function shopifyWorkspaceCommandApiPath(command) {
|
|
8062
|
+
return command === 'shopify workspace deploy-source' ? 'deploy' : 'validate';
|
|
8063
|
+
}
|
|
8064
|
+
async function requestShopifyWorkspaceSourceCommand(input) {
|
|
8065
|
+
const url = `${input.baseUrl}/api/account/connectors/${encodeURIComponent(input.connectorId)}/source/${shopifyWorkspaceCommandApiPath(input.command)}`;
|
|
8066
|
+
const response = await input.fetcher(url, {
|
|
8067
|
+
method: 'POST',
|
|
8068
|
+
headers: hostedWorkspaceApiHeaders({
|
|
8069
|
+
jsonBody: true,
|
|
8070
|
+
}),
|
|
8071
|
+
body: JSON.stringify({
|
|
8072
|
+
sourceArtifactId: input.sourceArtifactId,
|
|
8073
|
+
runtimeArtifactId: input.runtimeArtifactId,
|
|
8074
|
+
}),
|
|
8075
|
+
});
|
|
8076
|
+
const payload = response.status === 204 ? { ok: true } : await parseJsonResponse(response);
|
|
8077
|
+
if (payload) {
|
|
8078
|
+
return { status: response.status, payload };
|
|
8079
|
+
}
|
|
8080
|
+
return {
|
|
8081
|
+
status: response.status,
|
|
8082
|
+
payload: {
|
|
8083
|
+
ok: false,
|
|
8084
|
+
reasonCode: `http_${response.status}`,
|
|
8085
|
+
message: `Shopify workspace request failed with HTTP ${response.status}.`,
|
|
8086
|
+
},
|
|
8087
|
+
};
|
|
8088
|
+
}
|
|
8089
|
+
async function requestShopifyWorkspaceConnectorsList(input) {
|
|
8090
|
+
const params = new URLSearchParams({ runtimeArtifactId: input.runtimeArtifactId });
|
|
8091
|
+
const url = `${input.baseUrl}/api/account/connectors?${params.toString()}`;
|
|
8092
|
+
const response = await input.fetcher(url, {
|
|
8093
|
+
method: 'GET',
|
|
8094
|
+
headers: hostedWorkspaceApiHeaders({}),
|
|
8095
|
+
});
|
|
8096
|
+
const payload = response.status === 204 ? { ok: true } : await parseJsonResponse(response);
|
|
8097
|
+
if (payload) {
|
|
8098
|
+
return { status: response.status, payload };
|
|
8099
|
+
}
|
|
8100
|
+
return {
|
|
8101
|
+
status: response.status,
|
|
8102
|
+
payload: {
|
|
8103
|
+
ok: false,
|
|
8104
|
+
reasonCode: `http_${response.status}`,
|
|
8105
|
+
message: `Shopify workspace connectors request failed with HTTP ${response.status}.`,
|
|
8106
|
+
},
|
|
8107
|
+
};
|
|
8108
|
+
}
|
|
8109
|
+
async function requestShopifyWorkspaceLinkRuntime(input) {
|
|
8110
|
+
const url = `${input.baseUrl}/api/account/connectors/${encodeURIComponent(input.connectorId)}/runtime-links`;
|
|
8111
|
+
const response = await input.fetcher(url, {
|
|
8112
|
+
method: 'POST',
|
|
8113
|
+
headers: hostedWorkspaceApiHeaders({
|
|
8114
|
+
jsonBody: true,
|
|
8115
|
+
}),
|
|
8116
|
+
body: JSON.stringify({ runtimeArtifactId: input.runtimeArtifactId }),
|
|
8117
|
+
});
|
|
8118
|
+
const payload = response.status === 204 ? { ok: true } : await parseJsonResponse(response);
|
|
8119
|
+
if (payload) {
|
|
8120
|
+
return { status: response.status, payload };
|
|
8121
|
+
}
|
|
8122
|
+
return {
|
|
8123
|
+
status: response.status,
|
|
8124
|
+
payload: {
|
|
8125
|
+
ok: false,
|
|
8126
|
+
reasonCode: `http_${response.status}`,
|
|
8127
|
+
message: `Shopify workspace link request failed with HTTP ${response.status}.`,
|
|
8128
|
+
},
|
|
8129
|
+
};
|
|
8130
|
+
}
|
|
7687
8131
|
function dbCorrectedExample(artifactId, dbPath) {
|
|
7688
8132
|
if (dbPath === 'capabilities') {
|
|
7689
8133
|
return `tender app db capabilities ${artifactId} --json`;
|
|
@@ -8290,10 +8734,208 @@ async function planSourceFiles(input) {
|
|
|
8290
8734
|
}
|
|
8291
8735
|
return planned.sort((left, right) => left.path.localeCompare(right.path));
|
|
8292
8736
|
}
|
|
8737
|
+
function buildAuthStatusNextCommand(profileName) {
|
|
8738
|
+
return `tender auth status --profile ${profileName} --json`;
|
|
8739
|
+
}
|
|
8740
|
+
function buildAuthLoginRetryCommand(profileName, pendingAuthorization) {
|
|
8741
|
+
const parts = [
|
|
8742
|
+
'tender auth login --device',
|
|
8743
|
+
`--base-url ${pendingAuthorization.baseUrl}`,
|
|
8744
|
+
`--profile ${pendingAuthorization.tokenProfile}`,
|
|
8745
|
+
];
|
|
8746
|
+
if (pendingAuthorization.artifactId) {
|
|
8747
|
+
parts.push(`--artifact ${pendingAuthorization.artifactId}`);
|
|
8748
|
+
}
|
|
8749
|
+
parts.push(`--ttl ${pendingAuthorization.ttl}`, `--save-profile ${profileName}`);
|
|
8750
|
+
if (pendingAuthorization.shopifyAppUrl) {
|
|
8751
|
+
parts.push(`--shopify-app-url ${pendingAuthorization.shopifyAppUrl}`);
|
|
8752
|
+
}
|
|
8753
|
+
parts.push('--json');
|
|
8754
|
+
return parts.join(' ');
|
|
8755
|
+
}
|
|
8756
|
+
function pendingDeviceAuthorizationOutput(input) {
|
|
8757
|
+
return {
|
|
8758
|
+
ok: false,
|
|
8759
|
+
command: input.command,
|
|
8760
|
+
status: 'authorization_pending',
|
|
8761
|
+
authenticated: false,
|
|
8762
|
+
pending: true,
|
|
8763
|
+
baseUrl: input.baseUrl,
|
|
8764
|
+
profile: input.pendingAuthorization.tokenProfile,
|
|
8765
|
+
saveProfile: input.profileName,
|
|
8766
|
+
pendingProfile: input.profileName,
|
|
8767
|
+
artifactId: input.pendingAuthorization.artifactId ?? null,
|
|
8768
|
+
shopifyAppUrl: input.pendingAuthorization.shopifyAppUrl ?? null,
|
|
8769
|
+
shopifyStore: input.pendingAuthorization.shopifyStore ?? null,
|
|
8770
|
+
userCode: input.pendingAuthorization.userCode,
|
|
8771
|
+
verificationUri: input.pendingAuthorization.verificationUri,
|
|
8772
|
+
verificationUriComplete: input.pendingAuthorization.verificationUriComplete,
|
|
8773
|
+
expiresAt: formatExpiry(input.pendingAuthorization.expiresAt),
|
|
8774
|
+
interval: input.pendingAuthorization.interval ?? null,
|
|
8775
|
+
configPath: input.configPath,
|
|
8776
|
+
nextCommand: buildAuthStatusNextCommand(input.profileName),
|
|
8777
|
+
};
|
|
8778
|
+
}
|
|
8779
|
+
function storedProfileFromOAuthToken(input) {
|
|
8780
|
+
return {
|
|
8781
|
+
baseUrl: input.baseUrl,
|
|
8782
|
+
token: input.tokenPayload.access_token ?? '',
|
|
8783
|
+
tokenPrefix: input.tokenPayload.token_prefix,
|
|
8784
|
+
tenantId: input.tokenPayload.tenant_id,
|
|
8785
|
+
artifactId: input.tokenPayload.artifact_id,
|
|
8786
|
+
expiresAt: input.tokenPayload.expires_at,
|
|
8787
|
+
permissions: splitScope(input.tokenPayload.scope),
|
|
8788
|
+
...(input.shopifyAppUrl ? { shopifyAppUrl: input.shopifyAppUrl } : {}),
|
|
8789
|
+
...(input.shopifyStore ? { shopifyStore: input.shopifyStore } : {}),
|
|
8790
|
+
};
|
|
8791
|
+
}
|
|
8792
|
+
function authLoginSuccessOutput(input) {
|
|
8793
|
+
return {
|
|
8794
|
+
ok: true,
|
|
8795
|
+
command: input.command,
|
|
8796
|
+
baseUrl: input.baseUrl,
|
|
8797
|
+
profile: input.tokenProfile,
|
|
8798
|
+
savedProfile: input.saveProfile,
|
|
8799
|
+
tokenPrefix: input.tokenPayload.token_prefix ?? null,
|
|
8800
|
+
tenantId: input.tokenPayload.tenant_id ?? null,
|
|
8801
|
+
artifactId: input.tokenPayload.artifact_id ?? null,
|
|
8802
|
+
permissions: splitScope(input.tokenPayload.scope),
|
|
8803
|
+
expiresAt: formatExpiry(input.tokenPayload.expires_at),
|
|
8804
|
+
shopifyAppUrl: input.shopifyAppUrl,
|
|
8805
|
+
shopifyStore: input.shopifyStore,
|
|
8806
|
+
configPath: input.configPath,
|
|
8807
|
+
};
|
|
8808
|
+
}
|
|
8809
|
+
function configWithoutPendingDeviceAuthorization(config, profileName) {
|
|
8810
|
+
const pendingDeviceAuthorizations = { ...(config.pendingDeviceAuthorizations ?? {}) };
|
|
8811
|
+
delete pendingDeviceAuthorizations[profileName];
|
|
8812
|
+
return {
|
|
8813
|
+
...config,
|
|
8814
|
+
pendingDeviceAuthorizations: Object.keys(pendingDeviceAuthorizations).length > 0 ? pendingDeviceAuthorizations : undefined,
|
|
8815
|
+
};
|
|
8816
|
+
}
|
|
8817
|
+
async function completePendingDeviceAuthorization(input) {
|
|
8818
|
+
const configPath = readConfigPath(input.env);
|
|
8819
|
+
if (input.pendingAuthorization.expiresAt <= input.now()) {
|
|
8820
|
+
const updatedConfig = configWithoutPendingDeviceAuthorization(input.config, input.profileName);
|
|
8821
|
+
await writeConfig(input.env, updatedConfig);
|
|
8822
|
+
const expiredResult = {
|
|
8823
|
+
ok: false,
|
|
8824
|
+
command: input.command,
|
|
8825
|
+
status: 'authorization_expired',
|
|
8826
|
+
authenticated: false,
|
|
8827
|
+
pending: false,
|
|
8828
|
+
profile: input.profileName,
|
|
8829
|
+
configPath,
|
|
8830
|
+
nextCommand: buildAuthLoginRetryCommand(input.profileName, input.pendingAuthorization),
|
|
8831
|
+
};
|
|
8832
|
+
if (input.json) {
|
|
8833
|
+
input.io.stdout(JSON.stringify(expiredResult, null, 2));
|
|
8834
|
+
}
|
|
8835
|
+
else {
|
|
8836
|
+
input.io.stdout([
|
|
8837
|
+
'authenticated: false',
|
|
8838
|
+
`profile: ${input.profileName}`,
|
|
8839
|
+
'pending_device_auth: expired',
|
|
8840
|
+
`next: ${expiredResult.nextCommand}`,
|
|
8841
|
+
].join('\n'));
|
|
8842
|
+
}
|
|
8843
|
+
return 1;
|
|
8844
|
+
}
|
|
8845
|
+
const tokenResponse = await input.fetcher(`${input.pendingAuthorization.baseUrl}/oauth/token`, {
|
|
8846
|
+
method: 'POST',
|
|
8847
|
+
headers: {
|
|
8848
|
+
'content-type': 'application/json',
|
|
8849
|
+
},
|
|
8850
|
+
body: JSON.stringify({
|
|
8851
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
8852
|
+
device_code: input.pendingAuthorization.deviceCode,
|
|
8853
|
+
}),
|
|
8854
|
+
});
|
|
8855
|
+
const tokenPayload = await parseJsonResponse(tokenResponse);
|
|
8856
|
+
if (tokenResponse.ok && tokenPayload?.access_token) {
|
|
8857
|
+
const profiles = input.config.profiles ?? {};
|
|
8858
|
+
profiles[input.profileName] = storedProfileFromOAuthToken({
|
|
8859
|
+
baseUrl: input.pendingAuthorization.baseUrl,
|
|
8860
|
+
tokenPayload,
|
|
8861
|
+
shopifyAppUrl: input.pendingAuthorization.shopifyAppUrl ?? null,
|
|
8862
|
+
shopifyStore: input.pendingAuthorization.shopifyStore ?? null,
|
|
8863
|
+
});
|
|
8864
|
+
const updatedConfig = configWithoutPendingDeviceAuthorization({
|
|
8865
|
+
...input.config,
|
|
8866
|
+
defaultProfile: input.config.defaultProfile ?? input.profileName,
|
|
8867
|
+
profiles,
|
|
8868
|
+
}, input.profileName);
|
|
8869
|
+
await writeConfig(input.env, updatedConfig);
|
|
8870
|
+
const result = authLoginSuccessOutput({
|
|
8871
|
+
command: input.command,
|
|
8872
|
+
baseUrl: input.pendingAuthorization.baseUrl,
|
|
8873
|
+
tokenProfile: input.pendingAuthorization.tokenProfile,
|
|
8874
|
+
saveProfile: input.profileName,
|
|
8875
|
+
tokenPayload,
|
|
8876
|
+
shopifyAppUrl: input.pendingAuthorization.shopifyAppUrl ?? null,
|
|
8877
|
+
shopifyStore: input.pendingAuthorization.shopifyStore ?? null,
|
|
8878
|
+
configPath,
|
|
8879
|
+
});
|
|
8880
|
+
if (input.json) {
|
|
8881
|
+
input.io.stdout(JSON.stringify(result, null, 2));
|
|
8882
|
+
}
|
|
8883
|
+
else {
|
|
8884
|
+
input.io.stdout([
|
|
8885
|
+
'authenticated: true',
|
|
8886
|
+
`profile: ${input.profileName}`,
|
|
8887
|
+
`tenant_id: ${result.tenantId ?? '(unknown)'}`,
|
|
8888
|
+
`app_id: ${result.artifactId ?? '(account)'}`,
|
|
8889
|
+
`permissions: ${result.permissions.join(' ') || '(unknown)'}`,
|
|
8890
|
+
`expires_at: ${result.expiresAt ?? '(unknown)'}`,
|
|
8891
|
+
].join('\n'));
|
|
8892
|
+
}
|
|
8893
|
+
return 0;
|
|
8894
|
+
}
|
|
8895
|
+
if (tokenPayload?.error === 'authorization_pending' || tokenPayload?.error === 'slow_down') {
|
|
8896
|
+
const result = pendingDeviceAuthorizationOutput({
|
|
8897
|
+
command: input.command,
|
|
8898
|
+
baseUrl: input.pendingAuthorization.baseUrl,
|
|
8899
|
+
profileName: input.profileName,
|
|
8900
|
+
pendingAuthorization: input.pendingAuthorization,
|
|
8901
|
+
configPath,
|
|
8902
|
+
});
|
|
8903
|
+
if (input.json) {
|
|
8904
|
+
input.io.stdout(JSON.stringify(result, null, 2));
|
|
8905
|
+
}
|
|
8906
|
+
else {
|
|
8907
|
+
input.io.stdout([
|
|
8908
|
+
'authenticated: false',
|
|
8909
|
+
`profile: ${input.profileName}`,
|
|
8910
|
+
`verification_url: ${input.pendingAuthorization.verificationUriComplete}`,
|
|
8911
|
+
`user_code: ${input.pendingAuthorization.userCode}`,
|
|
8912
|
+
`next: ${result.nextCommand}`,
|
|
8913
|
+
].join('\n'));
|
|
8914
|
+
}
|
|
8915
|
+
return 1;
|
|
8916
|
+
}
|
|
8917
|
+
throw new Error(tokenPayload?.error_description ?? tokenPayload?.error ?? `Token exchange failed with HTTP ${tokenResponse.status}.`);
|
|
8918
|
+
}
|
|
8293
8919
|
async function runAuthStatus(command, io, runtime) {
|
|
8294
8920
|
const env = runtime.env ?? process.env;
|
|
8921
|
+
const fetcher = runtime.fetcher ?? fetch;
|
|
8922
|
+
const now = runtime.now ?? Date.now;
|
|
8295
8923
|
if (command.profileName) {
|
|
8296
8924
|
const config = await readConfig(env);
|
|
8925
|
+
const pendingAuthorization = config.pendingDeviceAuthorizations?.[command.profileName] ?? null;
|
|
8926
|
+
if (pendingAuthorization) {
|
|
8927
|
+
return await completePendingDeviceAuthorization({
|
|
8928
|
+
command: command.command,
|
|
8929
|
+
profileName: command.profileName,
|
|
8930
|
+
config,
|
|
8931
|
+
pendingAuthorization,
|
|
8932
|
+
env,
|
|
8933
|
+
fetcher,
|
|
8934
|
+
io,
|
|
8935
|
+
json: command.json,
|
|
8936
|
+
now,
|
|
8937
|
+
});
|
|
8938
|
+
}
|
|
8297
8939
|
const profile = config.profiles?.[command.profileName] ?? null;
|
|
8298
8940
|
const result = {
|
|
8299
8941
|
ok: Boolean(profile),
|
|
@@ -8355,6 +8997,20 @@ async function runAuthStatus(command, io, runtime) {
|
|
|
8355
8997
|
}
|
|
8356
8998
|
const config = await readConfig(env);
|
|
8357
8999
|
const profileName = command.profileName ?? config.defaultProfile ?? DEFAULT_PROFILE_NAME;
|
|
9000
|
+
const pendingAuthorization = config.pendingDeviceAuthorizations?.[profileName] ?? null;
|
|
9001
|
+
if (pendingAuthorization) {
|
|
9002
|
+
return await completePendingDeviceAuthorization({
|
|
9003
|
+
command: command.command,
|
|
9004
|
+
profileName,
|
|
9005
|
+
config,
|
|
9006
|
+
pendingAuthorization,
|
|
9007
|
+
env,
|
|
9008
|
+
fetcher,
|
|
9009
|
+
io,
|
|
9010
|
+
json: command.json,
|
|
9011
|
+
now,
|
|
9012
|
+
});
|
|
9013
|
+
}
|
|
8358
9014
|
const profile = config.profiles?.[profileName] ?? null;
|
|
8359
9015
|
const result = {
|
|
8360
9016
|
ok: Boolean(profile),
|
|
@@ -8419,7 +9075,11 @@ async function runAuthLogin(command, io, runtime) {
|
|
|
8419
9075
|
}),
|
|
8420
9076
|
});
|
|
8421
9077
|
const devicePayload = await parseJsonResponse(deviceResponse);
|
|
8422
|
-
if (!deviceResponse.ok ||
|
|
9078
|
+
if (!deviceResponse.ok ||
|
|
9079
|
+
!devicePayload?.device_code ||
|
|
9080
|
+
!devicePayload.user_code ||
|
|
9081
|
+
!devicePayload.verification_uri ||
|
|
9082
|
+
!devicePayload.verification_uri_complete) {
|
|
8423
9083
|
throw new Error(devicePayload?.error_description ?? devicePayload?.error ?? `Device authorization failed with HTTP ${deviceResponse.status}.`);
|
|
8424
9084
|
}
|
|
8425
9085
|
const verificationUriComplete = shopifyAppUrl
|
|
@@ -8429,19 +9089,51 @@ async function runAuthLogin(command, io, runtime) {
|
|
|
8429
9089
|
})
|
|
8430
9090
|
: devicePayload.verification_uri_complete;
|
|
8431
9091
|
const verificationUri = shopifyAppUrl ? shopifyDeviceVerificationUri(shopifyAppUrl) : devicePayload.verification_uri;
|
|
9092
|
+
const expiresAtMs = now() + Math.max(1, devicePayload.expires_in ?? 600) * 1000;
|
|
9093
|
+
const pendingAuthorization = {
|
|
9094
|
+
baseUrl,
|
|
9095
|
+
deviceCode: devicePayload.device_code,
|
|
9096
|
+
userCode: devicePayload.user_code,
|
|
9097
|
+
verificationUri,
|
|
9098
|
+
verificationUriComplete,
|
|
9099
|
+
expiresAt: expiresAtMs,
|
|
9100
|
+
interval: devicePayload.interval,
|
|
9101
|
+
tokenProfile: command.tokenProfile,
|
|
9102
|
+
artifactId: command.artifactId,
|
|
9103
|
+
ttl: command.ttl,
|
|
9104
|
+
...(shopifyAppUrl ? { shopifyAppUrl } : {}),
|
|
9105
|
+
...(shopifyStore ? { shopifyStore } : {}),
|
|
9106
|
+
};
|
|
9107
|
+
const pendingDeviceAuthorizations = {
|
|
9108
|
+
...(config.pendingDeviceAuthorizations ?? {}),
|
|
9109
|
+
[command.saveProfile]: pendingAuthorization,
|
|
9110
|
+
};
|
|
9111
|
+
const configWithPendingAuthorization = {
|
|
9112
|
+
...config,
|
|
9113
|
+
pendingDeviceAuthorizations: pendingDeviceAuthorizations,
|
|
9114
|
+
};
|
|
9115
|
+
await writeConfig(env, configWithPendingAuthorization);
|
|
8432
9116
|
const pendingResult = {
|
|
8433
|
-
ok:
|
|
9117
|
+
ok: false,
|
|
8434
9118
|
command: command.command,
|
|
9119
|
+
status: 'authorization_pending',
|
|
9120
|
+
authenticated: false,
|
|
9121
|
+
pending: true,
|
|
8435
9122
|
baseUrl,
|
|
8436
9123
|
shopifyAppUrl,
|
|
8437
9124
|
shopifyStore,
|
|
8438
9125
|
profile: command.tokenProfile,
|
|
8439
9126
|
saveProfile: command.saveProfile,
|
|
9127
|
+
pendingProfile: command.saveProfile,
|
|
9128
|
+
artifactId: command.artifactId,
|
|
8440
9129
|
userCode: devicePayload.user_code,
|
|
8441
9130
|
verificationUri,
|
|
8442
9131
|
verificationUriComplete,
|
|
8443
9132
|
expiresIn: devicePayload.expires_in ?? null,
|
|
9133
|
+
expiresAt: formatExpiry(expiresAtMs),
|
|
8444
9134
|
interval: devicePayload.interval ?? null,
|
|
9135
|
+
configPath: readConfigPath(env),
|
|
9136
|
+
nextCommand: buildAuthStatusNextCommand(command.saveProfile),
|
|
8445
9137
|
};
|
|
8446
9138
|
if (command.noPoll) {
|
|
8447
9139
|
if (command.json) {
|
|
@@ -8452,14 +9144,16 @@ async function runAuthLogin(command, io, runtime) {
|
|
|
8452
9144
|
}
|
|
8453
9145
|
return 0;
|
|
8454
9146
|
}
|
|
8455
|
-
if (
|
|
9147
|
+
if (command.json) {
|
|
9148
|
+
io.stderr(JSON.stringify(pendingResult, null, 2));
|
|
9149
|
+
}
|
|
9150
|
+
else {
|
|
8456
9151
|
io.stdout([
|
|
8457
9152
|
`Open this URL to approve Tender CLI access: ${verificationUriComplete}`,
|
|
8458
9153
|
`User code: ${devicePayload.user_code}`,
|
|
8459
9154
|
].join('\n'));
|
|
8460
9155
|
}
|
|
8461
9156
|
let intervalSeconds = Math.max(1, devicePayload.interval ?? 5);
|
|
8462
|
-
const expiresAtMs = now() + Math.max(1, devicePayload.expires_in ?? 600) * 1000;
|
|
8463
9157
|
let attemptsRemaining = Math.max(1, Math.ceil(command.pollTimeoutSeconds / intervalSeconds));
|
|
8464
9158
|
while (attemptsRemaining > 0 && now() < expiresAtMs) {
|
|
8465
9159
|
attemptsRemaining -= 1;
|
|
@@ -8475,38 +9169,29 @@ async function runAuthLogin(command, io, runtime) {
|
|
|
8475
9169
|
});
|
|
8476
9170
|
const tokenPayload = await parseJsonResponse(tokenResponse);
|
|
8477
9171
|
if (tokenResponse.ok && tokenPayload?.access_token) {
|
|
8478
|
-
const profiles =
|
|
8479
|
-
profiles[command.saveProfile] = {
|
|
9172
|
+
const profiles = configWithPendingAuthorization.profiles ?? {};
|
|
9173
|
+
profiles[command.saveProfile] = storedProfileFromOAuthToken({
|
|
8480
9174
|
baseUrl,
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
artifactId: tokenPayload.artifact_id,
|
|
8485
|
-
expiresAt: tokenPayload.expires_at,
|
|
8486
|
-
permissions: splitScope(tokenPayload.scope),
|
|
8487
|
-
...(shopifyAppUrl ? { shopifyAppUrl } : {}),
|
|
8488
|
-
...(shopifyStore ? { shopifyStore } : {}),
|
|
8489
|
-
};
|
|
8490
|
-
await writeConfig(env, {
|
|
8491
|
-
...config,
|
|
8492
|
-
defaultProfile: config.defaultProfile ?? command.saveProfile,
|
|
8493
|
-
profiles,
|
|
9175
|
+
tokenPayload,
|
|
9176
|
+
shopifyAppUrl,
|
|
9177
|
+
shopifyStore,
|
|
8494
9178
|
});
|
|
8495
|
-
const
|
|
8496
|
-
|
|
9179
|
+
const updatedConfig = configWithoutPendingDeviceAuthorization({
|
|
9180
|
+
...configWithPendingAuthorization,
|
|
9181
|
+
defaultProfile: configWithPendingAuthorization.defaultProfile ?? command.saveProfile,
|
|
9182
|
+
profiles,
|
|
9183
|
+
}, command.saveProfile);
|
|
9184
|
+
await writeConfig(env, updatedConfig);
|
|
9185
|
+
const result = authLoginSuccessOutput({
|
|
8497
9186
|
command: command.command,
|
|
8498
9187
|
baseUrl,
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
tenantId: tokenPayload.tenant_id ?? null,
|
|
8503
|
-
artifactId: tokenPayload.artifact_id ?? null,
|
|
8504
|
-
permissions: splitScope(tokenPayload.scope),
|
|
8505
|
-
expiresAt: formatExpiry(tokenPayload.expires_at),
|
|
9188
|
+
tokenProfile: command.tokenProfile,
|
|
9189
|
+
saveProfile: command.saveProfile,
|
|
9190
|
+
tokenPayload,
|
|
8506
9191
|
shopifyAppUrl,
|
|
8507
9192
|
shopifyStore,
|
|
8508
9193
|
configPath: readConfigPath(env),
|
|
8509
|
-
};
|
|
9194
|
+
});
|
|
8510
9195
|
if (command.json) {
|
|
8511
9196
|
io.stdout(JSON.stringify(result, null, 2));
|
|
8512
9197
|
}
|
|
@@ -9144,6 +9829,48 @@ async function runGitCommand(input) {
|
|
|
9144
9829
|
}
|
|
9145
9830
|
return result;
|
|
9146
9831
|
}
|
|
9832
|
+
async function resolveGitCommitArgument(input) {
|
|
9833
|
+
const value = input.value.trim();
|
|
9834
|
+
if (GIT_COMMIT_SHA_PATTERN.test(value)) {
|
|
9835
|
+
return value.toLowerCase();
|
|
9836
|
+
}
|
|
9837
|
+
if (!value || value.length > 256 || /[\0\r\n]/.test(value)) {
|
|
9838
|
+
throw new TenderCliUsageError('--commit must be a 40-character commit SHA or a Git ref resolvable in this checkout.');
|
|
9839
|
+
}
|
|
9840
|
+
const result = await runGitCommand({
|
|
9841
|
+
runtime: input.runtime,
|
|
9842
|
+
cwd: input.cwd,
|
|
9843
|
+
args: ['rev-parse', '--verify', `${value}^{commit}`],
|
|
9844
|
+
allowFailure: true,
|
|
9845
|
+
});
|
|
9846
|
+
const commitSha = result.stdout.trim().split(/\s+/)[0] ?? '';
|
|
9847
|
+
if (GIT_COMMIT_SHA_PATTERN.test(commitSha)) {
|
|
9848
|
+
return commitSha.toLowerCase();
|
|
9849
|
+
}
|
|
9850
|
+
const detail = result.stderr.trim() || result.stdout.trim();
|
|
9851
|
+
throw new TenderCliUsageError([
|
|
9852
|
+
`--commit must be a 40-character commit SHA or a Git ref resolvable in this checkout: ${value}`,
|
|
9853
|
+
detail,
|
|
9854
|
+
]
|
|
9855
|
+
.filter(Boolean)
|
|
9856
|
+
.join('\n'));
|
|
9857
|
+
}
|
|
9858
|
+
function readCommandCommitSha(command) {
|
|
9859
|
+
const commitSha = 'commitSha' in command ? command.commitSha : null;
|
|
9860
|
+
return typeof commitSha === 'string' && commitSha.trim() ? commitSha : null;
|
|
9861
|
+
}
|
|
9862
|
+
async function resolveCommandCommitSha(command, runtime) {
|
|
9863
|
+
const requestedCommitSha = readCommandCommitSha(command);
|
|
9864
|
+
if (!requestedCommitSha) {
|
|
9865
|
+
return command;
|
|
9866
|
+
}
|
|
9867
|
+
const commitSha = await resolveGitCommitArgument({
|
|
9868
|
+
runtime,
|
|
9869
|
+
cwd: process.cwd(),
|
|
9870
|
+
value: requestedCommitSha,
|
|
9871
|
+
});
|
|
9872
|
+
return commitSha === requestedCommitSha ? command : { ...command, commitSha };
|
|
9873
|
+
}
|
|
9147
9874
|
function shellQuote(value) {
|
|
9148
9875
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
9149
9876
|
}
|
|
@@ -10004,29 +10731,30 @@ async function runArtifactsInit(command, io, runtime) {
|
|
|
10004
10731
|
return 0;
|
|
10005
10732
|
}
|
|
10006
10733
|
async function postLifecycle(input) {
|
|
10007
|
-
const
|
|
10008
|
-
|
|
10009
|
-
|
|
10010
|
-
|
|
10734
|
+
const command = await resolveCommandCommitSha(input.command, input.runtime);
|
|
10735
|
+
const credentials = await resolveLifecycleApiCredentials({
|
|
10736
|
+
baseUrl: command.baseUrl,
|
|
10737
|
+
flagToken: command.flagToken,
|
|
10738
|
+
profileName: command.profileName,
|
|
10011
10739
|
env: input.runtime.env ?? process.env,
|
|
10012
10740
|
});
|
|
10013
10741
|
const fetcher = input.runtime.fetcher ?? fetch;
|
|
10014
|
-
const stream =
|
|
10742
|
+
const stream = command.stream;
|
|
10015
10743
|
const url = `${credentials.baseUrl}${input.path}${stream ? '?stream=1' : ''}`;
|
|
10016
10744
|
const requestBody = {};
|
|
10017
|
-
if ('commitSha' in
|
|
10018
|
-
requestBody.commitSha =
|
|
10745
|
+
if ('commitSha' in command && command.commitSha) {
|
|
10746
|
+
requestBody.commitSha = command.commitSha;
|
|
10019
10747
|
}
|
|
10020
|
-
if ('message' in
|
|
10021
|
-
requestBody.message =
|
|
10748
|
+
if ('message' in command && command.message) {
|
|
10749
|
+
requestBody.message = command.message;
|
|
10022
10750
|
}
|
|
10023
10751
|
const response = await fetcher(url, {
|
|
10024
10752
|
method: 'POST',
|
|
10025
|
-
headers: {
|
|
10026
|
-
|
|
10753
|
+
headers: apiHeadersForCredentials({
|
|
10754
|
+
credentials,
|
|
10027
10755
|
accept: stream ? 'text/event-stream' : 'application/json',
|
|
10028
|
-
|
|
10029
|
-
},
|
|
10756
|
+
jsonBody: Object.keys(requestBody).length > 0,
|
|
10757
|
+
}),
|
|
10030
10758
|
body: Object.keys(requestBody).length > 0 ? JSON.stringify(requestBody) : undefined,
|
|
10031
10759
|
});
|
|
10032
10760
|
if (stream) {
|
|
@@ -10036,19 +10764,19 @@ async function postLifecycle(input) {
|
|
|
10036
10764
|
await emitSseLifecycleStream({
|
|
10037
10765
|
response,
|
|
10038
10766
|
io: input.io,
|
|
10039
|
-
json:
|
|
10767
|
+
json: command.json,
|
|
10040
10768
|
});
|
|
10041
10769
|
await appendAutomaticMemory({
|
|
10042
10770
|
repoPath: process.cwd(),
|
|
10043
|
-
artifactId:
|
|
10771
|
+
artifactId: command.artifactId,
|
|
10044
10772
|
kind: input.operation === 'publish' ? 'change' : 'validation',
|
|
10045
|
-
summary: `${
|
|
10773
|
+
summary: `${command.command} streamed`,
|
|
10046
10774
|
body: `Operation: ${input.operation}\nStatus: streamed`,
|
|
10047
10775
|
tags: [input.operation, 'stream'],
|
|
10048
10776
|
status: 'resolved',
|
|
10049
10777
|
related: {
|
|
10050
|
-
commands: [
|
|
10051
|
-
...('commitSha' in
|
|
10778
|
+
commands: [command.command],
|
|
10779
|
+
...('commitSha' in command && command.commitSha ? { releaseId: command.commitSha } : {}),
|
|
10052
10780
|
},
|
|
10053
10781
|
runtime: input.runtime,
|
|
10054
10782
|
});
|
|
@@ -10066,12 +10794,12 @@ async function postLifecycle(input) {
|
|
|
10066
10794
|
}
|
|
10067
10795
|
const result = {
|
|
10068
10796
|
ok: true,
|
|
10069
|
-
command:
|
|
10797
|
+
command: command.command,
|
|
10070
10798
|
baseUrl: credentials.baseUrl,
|
|
10071
10799
|
authSource: credentials.source,
|
|
10072
10800
|
profile: credentials.profileName,
|
|
10073
|
-
artifactId:
|
|
10074
|
-
appId:
|
|
10801
|
+
artifactId: command.artifactId,
|
|
10802
|
+
appId: command.artifactId,
|
|
10075
10803
|
operation: payload.operation ?? input.operation,
|
|
10076
10804
|
job: payload.job ?? null,
|
|
10077
10805
|
result: payload.result ?? null,
|
|
@@ -10081,15 +10809,15 @@ async function postLifecycle(input) {
|
|
|
10081
10809
|
const releaseGitRef = input.operation === 'publish' ? readReleaseGitRefFromResult(result.result) : null;
|
|
10082
10810
|
const memory = await appendAutomaticMemory({
|
|
10083
10811
|
repoPath,
|
|
10084
|
-
artifactId:
|
|
10812
|
+
artifactId: command.artifactId,
|
|
10085
10813
|
kind: input.operation === 'validate' ? 'validation' : input.operation === 'publish' ? 'change' : 'validation',
|
|
10086
|
-
summary: `${
|
|
10814
|
+
summary: `${command.command} completed`,
|
|
10087
10815
|
body: `Operation: ${result.operation}\nStatus: completed`,
|
|
10088
10816
|
tags: [input.operation],
|
|
10089
10817
|
status: 'resolved',
|
|
10090
10818
|
related: {
|
|
10091
10819
|
files: changedFiles,
|
|
10092
|
-
commands: [
|
|
10820
|
+
commands: [command.command],
|
|
10093
10821
|
...(input.operation === 'preview' ? { previewUrl: readPreviewUrlFromJob(result.job) ?? undefined } : {}),
|
|
10094
10822
|
...(input.operation === 'publish' ? { publishedUrl: readObjectString(readObject(result.result), 'publishedUrl') ?? undefined } : {}),
|
|
10095
10823
|
...(releaseGitRef?.tagName ? { releaseId: releaseGitRef.tagName } : {}),
|
|
@@ -10104,12 +10832,12 @@ async function postLifecycle(input) {
|
|
|
10104
10832
|
memory,
|
|
10105
10833
|
...(memorySync ? { memorySync } : {}),
|
|
10106
10834
|
};
|
|
10107
|
-
if (
|
|
10835
|
+
if (command.json) {
|
|
10108
10836
|
input.io.stdout(JSON.stringify(resultWithMemory, null, 2));
|
|
10109
10837
|
}
|
|
10110
10838
|
else {
|
|
10111
10839
|
input.io.stdout([
|
|
10112
|
-
`app_id: ${
|
|
10840
|
+
`app_id: ${command.artifactId}`,
|
|
10113
10841
|
`operation: ${result.operation}`,
|
|
10114
10842
|
'status: completed',
|
|
10115
10843
|
...(input.operation === 'publish' ? formatPublishResultLines(result.result) : []),
|
|
@@ -10204,35 +10932,36 @@ async function requestPreviewStatus(input) {
|
|
|
10204
10932
|
};
|
|
10205
10933
|
}
|
|
10206
10934
|
async function runPreviewStatus(command, io, runtime) {
|
|
10935
|
+
const resolvedCommand = await resolveCommandCommitSha(command, runtime);
|
|
10207
10936
|
const credentials = await resolveApiCredentials({
|
|
10208
|
-
baseUrl:
|
|
10209
|
-
flagToken:
|
|
10210
|
-
profileName:
|
|
10937
|
+
baseUrl: resolvedCommand.baseUrl,
|
|
10938
|
+
flagToken: resolvedCommand.flagToken,
|
|
10939
|
+
profileName: resolvedCommand.profileName,
|
|
10211
10940
|
env: runtime.env ?? process.env,
|
|
10212
10941
|
});
|
|
10213
10942
|
const { job, preview } = await requestPreviewStatus({
|
|
10214
|
-
command,
|
|
10943
|
+
command: resolvedCommand,
|
|
10215
10944
|
credentials,
|
|
10216
10945
|
fetcher: runtime.fetcher ?? fetch,
|
|
10217
10946
|
});
|
|
10218
10947
|
const result = {
|
|
10219
10948
|
ok: true,
|
|
10220
|
-
command:
|
|
10949
|
+
command: resolvedCommand.command,
|
|
10221
10950
|
baseUrl: credentials.baseUrl,
|
|
10222
10951
|
authSource: credentials.source,
|
|
10223
10952
|
profile: credentials.profileName,
|
|
10224
|
-
artifactId:
|
|
10225
|
-
commitSha: preview.commitSha ??
|
|
10953
|
+
artifactId: resolvedCommand.artifactId,
|
|
10954
|
+
commitSha: preview.commitSha ?? resolvedCommand.commitSha,
|
|
10226
10955
|
status: preview.status ?? job.status ?? null,
|
|
10227
10956
|
previewUrl: preview.previewUrl ?? readPreviewUrlFromJob(job),
|
|
10228
10957
|
job,
|
|
10229
10958
|
};
|
|
10230
|
-
if (
|
|
10959
|
+
if (resolvedCommand.json) {
|
|
10231
10960
|
io.stdout(JSON.stringify(result, null, 2));
|
|
10232
10961
|
}
|
|
10233
10962
|
else {
|
|
10234
10963
|
io.stdout([
|
|
10235
|
-
`app_id: ${
|
|
10964
|
+
`app_id: ${resolvedCommand.artifactId}`,
|
|
10236
10965
|
`job_id: ${job.jobId}`,
|
|
10237
10966
|
`status: ${result.status ?? 'unknown'}`,
|
|
10238
10967
|
...(result.previewUrl ? [`preview_url: ${result.previewUrl}`] : []),
|
|
@@ -10241,19 +10970,20 @@ async function runPreviewStatus(command, io, runtime) {
|
|
|
10241
10970
|
return 0;
|
|
10242
10971
|
}
|
|
10243
10972
|
async function runPreviewWatch(command, io, runtime) {
|
|
10973
|
+
const resolvedCommand = await resolveCommandCommitSha(command, runtime);
|
|
10244
10974
|
const credentials = await resolveApiCredentials({
|
|
10245
|
-
baseUrl:
|
|
10246
|
-
flagToken:
|
|
10247
|
-
profileName:
|
|
10975
|
+
baseUrl: resolvedCommand.baseUrl,
|
|
10976
|
+
flagToken: resolvedCommand.flagToken,
|
|
10977
|
+
profileName: resolvedCommand.profileName,
|
|
10248
10978
|
env: runtime.env ?? process.env,
|
|
10249
10979
|
});
|
|
10250
10980
|
const { job } = await requestPreviewStatus({
|
|
10251
|
-
command,
|
|
10981
|
+
command: resolvedCommand,
|
|
10252
10982
|
credentials,
|
|
10253
10983
|
fetcher: runtime.fetcher ?? fetch,
|
|
10254
10984
|
});
|
|
10255
|
-
const stream =
|
|
10256
|
-
const response = await (runtime.fetcher ?? fetch)(`${credentials.baseUrl}/api/v1/artifacts/${encodeURIComponent(
|
|
10985
|
+
const stream = resolvedCommand.stream;
|
|
10986
|
+
const response = await (runtime.fetcher ?? fetch)(`${credentials.baseUrl}/api/v1/artifacts/${encodeURIComponent(resolvedCommand.artifactId)}/jobs/${encodeURIComponent(job.jobId)}/events${stream ? '?stream=1' : ''}`, {
|
|
10257
10987
|
headers: {
|
|
10258
10988
|
authorization: `Bearer ${credentials.token}`,
|
|
10259
10989
|
accept: stream ? 'text/event-stream' : 'application/json',
|
|
@@ -10266,7 +10996,7 @@ async function runPreviewWatch(command, io, runtime) {
|
|
|
10266
10996
|
await emitSseLifecycleStream({
|
|
10267
10997
|
response,
|
|
10268
10998
|
io,
|
|
10269
|
-
json:
|
|
10999
|
+
json: resolvedCommand.json,
|
|
10270
11000
|
});
|
|
10271
11001
|
return 0;
|
|
10272
11002
|
}
|
|
@@ -10276,15 +11006,15 @@ async function runPreviewWatch(command, io, runtime) {
|
|
|
10276
11006
|
}
|
|
10277
11007
|
const result = {
|
|
10278
11008
|
ok: true,
|
|
10279
|
-
command:
|
|
11009
|
+
command: resolvedCommand.command,
|
|
10280
11010
|
baseUrl: credentials.baseUrl,
|
|
10281
11011
|
authSource: credentials.source,
|
|
10282
11012
|
profile: credentials.profileName,
|
|
10283
|
-
artifactId:
|
|
11013
|
+
artifactId: resolvedCommand.artifactId,
|
|
10284
11014
|
job: payload.job ?? job,
|
|
10285
11015
|
events: payload.events ?? [],
|
|
10286
11016
|
};
|
|
10287
|
-
if (
|
|
11017
|
+
if (resolvedCommand.json) {
|
|
10288
11018
|
io.stdout(JSON.stringify(result, null, 2));
|
|
10289
11019
|
}
|
|
10290
11020
|
else {
|
|
@@ -10293,38 +11023,39 @@ async function runPreviewWatch(command, io, runtime) {
|
|
|
10293
11023
|
return 0;
|
|
10294
11024
|
}
|
|
10295
11025
|
async function runPublish(command, io, runtime) {
|
|
10296
|
-
|
|
11026
|
+
const resolvedCommand = await resolveCommandCommitSha(command, runtime);
|
|
11027
|
+
if (resolvedCommand.dryRun) {
|
|
10297
11028
|
const result = {
|
|
10298
11029
|
ok: true,
|
|
10299
|
-
command:
|
|
10300
|
-
artifactId:
|
|
10301
|
-
appId:
|
|
10302
|
-
commitSha:
|
|
11030
|
+
command: resolvedCommand.command,
|
|
11031
|
+
artifactId: resolvedCommand.artifactId,
|
|
11032
|
+
appId: resolvedCommand.artifactId,
|
|
11033
|
+
commitSha: resolvedCommand.commitSha,
|
|
10303
11034
|
dryRun: true,
|
|
10304
11035
|
confirmed: false,
|
|
10305
|
-
message:
|
|
11036
|
+
message: resolvedCommand.message,
|
|
10306
11037
|
wouldRequest: {
|
|
10307
11038
|
method: 'POST',
|
|
10308
|
-
path: `/api/v1/artifacts/${
|
|
10309
|
-
stream:
|
|
11039
|
+
path: `/api/v1/artifacts/${resolvedCommand.artifactId}/publish`,
|
|
11040
|
+
stream: resolvedCommand.stream,
|
|
10310
11041
|
body: {
|
|
10311
|
-
...(
|
|
10312
|
-
...(
|
|
11042
|
+
...(resolvedCommand.commitSha ? { commitSha: resolvedCommand.commitSha } : {}),
|
|
11043
|
+
...(resolvedCommand.message ? { message: resolvedCommand.message } : {}),
|
|
10313
11044
|
},
|
|
10314
11045
|
},
|
|
10315
11046
|
};
|
|
10316
|
-
if (
|
|
11047
|
+
if (resolvedCommand.json) {
|
|
10317
11048
|
io.stdout(JSON.stringify(result, null, 2));
|
|
10318
11049
|
}
|
|
10319
11050
|
else {
|
|
10320
|
-
io.stdout([`app_id: ${
|
|
11051
|
+
io.stdout([`app_id: ${resolvedCommand.artifactId}`, 'dry_run: true', 'next: rerun with --confirm publish to publish.'].join('\n'));
|
|
10321
11052
|
}
|
|
10322
11053
|
return 0;
|
|
10323
11054
|
}
|
|
10324
11055
|
return await postLifecycle({
|
|
10325
|
-
command,
|
|
11056
|
+
command: resolvedCommand,
|
|
10326
11057
|
operation: 'publish',
|
|
10327
|
-
path: `/api/v1/artifacts/${encodeURIComponent(
|
|
11058
|
+
path: `/api/v1/artifacts/${encodeURIComponent(resolvedCommand.artifactId)}/publish`,
|
|
10328
11059
|
io,
|
|
10329
11060
|
runtime,
|
|
10330
11061
|
});
|
|
@@ -10994,7 +11725,7 @@ async function resolvePlaybooksCredentials(command, runtime) {
|
|
|
10994
11725
|
if (error instanceof TenderCliUsageError) {
|
|
10995
11726
|
throw new TenderCliUsageError([
|
|
10996
11727
|
'Playbooks require Tender authentication.',
|
|
10997
|
-
'Run `tender auth login --device --profile edit-preview --save-profile agent --ttl 7d --json`, set TENDER_API_TOKEN
|
|
11728
|
+
'Run `tender auth login --device --profile edit-preview --save-profile agent --ttl 7d --json`, approve the URL, then run `tender auth status --profile agent --json`; or set TENDER_API_TOKEN or pass --token <token>.',
|
|
10998
11729
|
].join('\n'));
|
|
10999
11730
|
}
|
|
11000
11731
|
throw error;
|
|
@@ -11098,7 +11829,7 @@ async function resolveConnectorsCredentials(command, runtime) {
|
|
|
11098
11829
|
if (error instanceof TenderCliUsageError) {
|
|
11099
11830
|
throw new TenderCliUsageError([
|
|
11100
11831
|
'Connectors require Tender account authentication.',
|
|
11101
|
-
'Run `tender auth login --device --profile create --ttl 7d --save-profile account --json`, set TENDER_API_TOKEN
|
|
11832
|
+
'Run `tender auth login --device --profile create --ttl 7d --save-profile account --json`, approve the URL, then run `tender auth status --profile account --json`; or set TENDER_API_TOKEN or pass --token <token>.',
|
|
11102
11833
|
].join('\n'));
|
|
11103
11834
|
}
|
|
11104
11835
|
throw error;
|
|
@@ -11123,6 +11854,7 @@ async function runConnectorsList(command, io, runtime) {
|
|
|
11123
11854
|
baseUrl: credentials.baseUrl,
|
|
11124
11855
|
token: credentials.token,
|
|
11125
11856
|
fetcher: runtime.fetcher ?? fetch,
|
|
11857
|
+
hostedWorkspaceHint: Boolean((runtime.env ?? process.env).TENDER_RUNTIME_ARTIFACT_ID),
|
|
11126
11858
|
});
|
|
11127
11859
|
const connectors = readConnectorArray(payload);
|
|
11128
11860
|
printConnectorsPayload(command, io, payload, connectors.length > 0
|
|
@@ -11418,6 +12150,7 @@ merchant-editable settings.
|
|
|
11418
12150
|
['extensions/tender-product-survey/package.json', '{\n "name": "tender-product-survey",\n "private": true,\n "version": "0.0.0"\n}\n'],
|
|
11419
12151
|
['extensions/tender-product-survey/shopify.extension.toml', extensionToml],
|
|
11420
12152
|
['extensions/tender-product-survey/blocks/product_survey.liquid', blockLiquid],
|
|
12153
|
+
['extensions/tender-product-survey/locales/en.default.json', '{}\n'],
|
|
11421
12154
|
]);
|
|
11422
12155
|
}
|
|
11423
12156
|
function buildShopifySourceFiles(input) {
|
|
@@ -11506,7 +12239,7 @@ spawned Shopify CLI child process.
|
|
|
11506
12239
|
## Validate
|
|
11507
12240
|
|
|
11508
12241
|
\`\`\`sh
|
|
11509
|
-
|
|
12242
|
+
npm exec --yes --package @shopify/cli@latest -- shopify app config validate --path . --json
|
|
11510
12243
|
\`\`\`
|
|
11511
12244
|
|
|
11512
12245
|
## Deploy
|
|
@@ -11514,6 +12247,17 @@ npx --yes @shopify/cli@latest app config validate --path . --json
|
|
|
11514
12247
|
\`\`\`sh
|
|
11515
12248
|
tender connectors shopify source deploy ${input.connectorId} --dir . --confirm deploy --profile account --json
|
|
11516
12249
|
\`\`\`
|
|
12250
|
+
|
|
12251
|
+
Inside the hosted Shopify workspace, commit and push source changes, then
|
|
12252
|
+
validate or deploy from the embedded Shopify app's Deploy source action so
|
|
12253
|
+
Tender can use the connector's stored App Automation Token server-side. If
|
|
12254
|
+
Shopify CLI is already authenticated in the hosted workspace, direct
|
|
12255
|
+
\`shopify app config validate\` and \`shopify app deploy\` are allowed. Do not run
|
|
12256
|
+
\`tender connectors shopify source validate\` or
|
|
12257
|
+
\`tender connectors shopify source deploy\` from hosted workspaces; those
|
|
12258
|
+
commands require an account-scoped Tender CLI token. Never ask for Shopify App
|
|
12259
|
+
Automation Tokens, client secrets, Tender tokens, or provider keys in the
|
|
12260
|
+
terminal.
|
|
11517
12261
|
`;
|
|
11518
12262
|
const files = new Map([
|
|
11519
12263
|
['.gitignore', '.shopify/\nnode_modules/\n.env\n.env.*\n'],
|
|
@@ -11757,8 +12501,6 @@ async function commitAndPushSourceFiles(input) {
|
|
|
11757
12501
|
}
|
|
11758
12502
|
function shopifyValidateArgs(input) {
|
|
11759
12503
|
return [
|
|
11760
|
-
'--yes',
|
|
11761
|
-
'@shopify/cli@latest',
|
|
11762
12504
|
'app',
|
|
11763
12505
|
'config',
|
|
11764
12506
|
'validate',
|
|
@@ -11770,8 +12512,6 @@ function shopifyValidateArgs(input) {
|
|
|
11770
12512
|
}
|
|
11771
12513
|
function shopifyDeployArgs(input) {
|
|
11772
12514
|
return [
|
|
11773
|
-
'--yes',
|
|
11774
|
-
'@shopify/cli@latest',
|
|
11775
12515
|
'app',
|
|
11776
12516
|
'deploy',
|
|
11777
12517
|
'--path',
|
|
@@ -11783,9 +12523,50 @@ function shopifyDeployArgs(input) {
|
|
|
11783
12523
|
...(input.sourceControlUrl ? ['--source-control-url', input.sourceControlUrl] : []),
|
|
11784
12524
|
];
|
|
11785
12525
|
}
|
|
12526
|
+
function shopifyCliCommand(args) {
|
|
12527
|
+
return {
|
|
12528
|
+
command: 'npm',
|
|
12529
|
+
args: ['exec', '--yes', '--package', SHOPIFY_CLI_PACKAGE, '--', SHOPIFY_CLI_BIN, ...args],
|
|
12530
|
+
};
|
|
12531
|
+
}
|
|
12532
|
+
function shopifyCliCommandLine(args) {
|
|
12533
|
+
const invocation = shopifyCliCommand(args);
|
|
12534
|
+
return commandLine(invocation.command, invocation.args);
|
|
12535
|
+
}
|
|
11786
12536
|
function commandLine(command, args) {
|
|
11787
12537
|
return [command, ...args.map(shellQuote)].join(' ');
|
|
11788
12538
|
}
|
|
12539
|
+
function shouldDropNestedNpmEnv(key) {
|
|
12540
|
+
const normalized = key.toLowerCase();
|
|
12541
|
+
return (normalized === 'init_cwd' ||
|
|
12542
|
+
normalized === 'npm_command' ||
|
|
12543
|
+
normalized === 'npm_config_argv' ||
|
|
12544
|
+
normalized === 'npm_config_call' ||
|
|
12545
|
+
normalized === 'npm_config_package' ||
|
|
12546
|
+
normalized === 'npm_execpath' ||
|
|
12547
|
+
normalized === 'npm_node_execpath' ||
|
|
12548
|
+
normalized.startsWith('npm_lifecycle_') ||
|
|
12549
|
+
normalized.startsWith('npm_package_'));
|
|
12550
|
+
}
|
|
12551
|
+
function sanitizeNestedNpmEnvironment(env) {
|
|
12552
|
+
const sanitized = {};
|
|
12553
|
+
for (const [key, value] of Object.entries(env)) {
|
|
12554
|
+
if (shouldDropNestedNpmEnv(key)) {
|
|
12555
|
+
continue;
|
|
12556
|
+
}
|
|
12557
|
+
sanitized[key] = value;
|
|
12558
|
+
}
|
|
12559
|
+
return sanitized;
|
|
12560
|
+
}
|
|
12561
|
+
function shopifyCliChildEnv(runtime, overrides = {}) {
|
|
12562
|
+
return {
|
|
12563
|
+
...sanitizeNestedNpmEnvironment({
|
|
12564
|
+
...process.env,
|
|
12565
|
+
...(runtime.env ?? {}),
|
|
12566
|
+
}),
|
|
12567
|
+
...overrides,
|
|
12568
|
+
};
|
|
12569
|
+
}
|
|
11789
12570
|
function redactSensitiveText(value, secrets = []) {
|
|
11790
12571
|
let redacted = value;
|
|
11791
12572
|
for (const secret of secrets) {
|
|
@@ -11799,11 +12580,12 @@ function redactSensitiveText(value, secrets = []) {
|
|
|
11799
12580
|
.replace(/\b(authorization\s*[:=]\s*(?:Bearer\s+)?)([^\s'"]+)/gi, '$1[REDACTED]')
|
|
11800
12581
|
.replace(/\b(x-shopify-access-token\s*[:=]\s*)([^\s'"]+)/gi, '$1[REDACTED]');
|
|
11801
12582
|
}
|
|
11802
|
-
async function
|
|
12583
|
+
async function runShopifyCliCommand(input) {
|
|
11803
12584
|
const runner = input.runtime.commandRunner ?? defaultCommandRunner;
|
|
11804
|
-
const
|
|
12585
|
+
const invocation = shopifyCliCommand(input.args);
|
|
12586
|
+
const result = await runner(invocation.command, invocation.args, {
|
|
11805
12587
|
cwd: input.cwd,
|
|
11806
|
-
|
|
12588
|
+
env: shopifyCliChildEnv(input.runtime, input.env),
|
|
11807
12589
|
});
|
|
11808
12590
|
const redactedResult = {
|
|
11809
12591
|
...result,
|
|
@@ -11811,12 +12593,13 @@ async function runNpxCommand(input) {
|
|
|
11811
12593
|
stderr: redactSensitiveText(result.stderr, input.redactions),
|
|
11812
12594
|
};
|
|
11813
12595
|
if (redactedResult.exitCode !== 0) {
|
|
11814
|
-
throw new
|
|
11815
|
-
|
|
11816
|
-
|
|
11817
|
-
|
|
11818
|
-
.
|
|
11819
|
-
.
|
|
12596
|
+
throw new TenderCliChildProcessError({
|
|
12597
|
+
command: invocation.command,
|
|
12598
|
+
args: invocation.args,
|
|
12599
|
+
exitCode: redactedResult.exitCode,
|
|
12600
|
+
stdout: redactedResult.stdout,
|
|
12601
|
+
stderr: redactedResult.stderr,
|
|
12602
|
+
});
|
|
11820
12603
|
}
|
|
11821
12604
|
return redactedResult;
|
|
11822
12605
|
}
|
|
@@ -11845,10 +12628,8 @@ function localShopifyDeployToken(runtime) {
|
|
|
11845
12628
|
const token = env.SHOPIFY_APP_AUTOMATION_TOKEN;
|
|
11846
12629
|
return token?.trim() ? token.trim() : null;
|
|
11847
12630
|
}
|
|
11848
|
-
function childShopifyDeployEnv(
|
|
12631
|
+
function childShopifyDeployEnv(_runtime, token) {
|
|
11849
12632
|
return {
|
|
11850
|
-
...process.env,
|
|
11851
|
-
...(runtime.env ?? {}),
|
|
11852
12633
|
SHOPIFY_APP_AUTOMATION_TOKEN: token,
|
|
11853
12634
|
};
|
|
11854
12635
|
}
|
|
@@ -11957,8 +12738,8 @@ async function runConnectorsShopifySourceInit(command, io, runtime) {
|
|
|
11957
12738
|
sourceCheckout: checkout.sourceCheckout,
|
|
11958
12739
|
commit: gitCommit,
|
|
11959
12740
|
},
|
|
11960
|
-
validateCommand:
|
|
11961
|
-
deployCommand:
|
|
12741
|
+
validateCommand: shopifyCliCommandLine(shopifyValidateArgs({ dir, clientId: command.clientId })),
|
|
12742
|
+
deployCommand: shopifyCliCommandLine(shopifyDeployArgs({ dir, clientId: command.clientId, version: 'connector-source', message: null, sourceControlUrl: null, allowDeletes: false })),
|
|
11962
12743
|
};
|
|
11963
12744
|
printConnectorsPayload(command, io, result, [
|
|
11964
12745
|
`connector_id: ${command.connectorId}`,
|
|
@@ -12020,10 +12801,10 @@ async function runConnectorsShopifySourceValidate(command, io, runtime) {
|
|
|
12020
12801
|
}
|
|
12021
12802
|
}
|
|
12022
12803
|
if (!command.execute) {
|
|
12023
|
-
printConnectorsPayload(command, io, { ok: true, dryRun: true, command: command.command, connectorId: command.connectorId, sourceArtifactId, dir, shopifyCommand:
|
|
12804
|
+
printConnectorsPayload(command, io, { ok: true, dryRun: true, command: command.command, connectorId: command.connectorId, sourceArtifactId, dir, shopifyCommand: shopifyCliCommandLine(args) }, [`connector_id: ${command.connectorId}`, `source_artifact_id: ${sourceArtifactId}`, `source_dir: ${dir}`, `shopify_command: ${shopifyCliCommandLine(args)}`]);
|
|
12024
12805
|
return 0;
|
|
12025
12806
|
}
|
|
12026
|
-
const result = await
|
|
12807
|
+
const result = await runShopifyCliCommand({ args, cwd: dir, runtime });
|
|
12027
12808
|
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}`, 'validation: passed']);
|
|
12028
12809
|
return 0;
|
|
12029
12810
|
}
|
|
@@ -12044,7 +12825,7 @@ async function runConnectorsShopifySourceDeploy(command, io, runtime) {
|
|
|
12044
12825
|
allowDeletes: command.allowDeletes,
|
|
12045
12826
|
});
|
|
12046
12827
|
if (command.confirm !== 'deploy') {
|
|
12047
|
-
printConnectorsPayload(command, io, { ok: true, dryRun: true, command: command.command, connectorId: command.connectorId, sourceArtifactId, dir, shopifyCommand:
|
|
12828
|
+
printConnectorsPayload(command, io, { ok: true, dryRun: true, command: command.command, connectorId: command.connectorId, sourceArtifactId, dir, shopifyCommand: shopifyCliCommandLine(args), confirmRequired: 'deploy' }, [`connector_id: ${command.connectorId}`, `source_artifact_id: ${sourceArtifactId}`, `source_dir: ${dir}`, `shopify_command: ${shopifyCliCommandLine(args)}`, 'confirm_required: --confirm deploy']);
|
|
12048
12829
|
return 0;
|
|
12049
12830
|
}
|
|
12050
12831
|
const deployToken = await resolveShopifyDeployTokenEnvironment({
|
|
@@ -12054,10 +12835,268 @@ async function runConnectorsShopifySourceDeploy(command, io, runtime) {
|
|
|
12054
12835
|
runtime,
|
|
12055
12836
|
fetcher,
|
|
12056
12837
|
});
|
|
12057
|
-
const result = await
|
|
12838
|
+
const result = await runShopifyCliCommand({ args, cwd: dir, runtime, env: deployToken.env, redactions: deployToken.redactions });
|
|
12058
12839
|
printConnectorsPayload(command, io, { ok: true, command: command.command, connectorId: command.connectorId, sourceArtifactId, dir, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr }, [`connector_id: ${command.connectorId}`, `source_artifact_id: ${sourceArtifactId}`, `source_dir: ${dir}`, 'deploy: completed']);
|
|
12059
12840
|
return 0;
|
|
12060
12841
|
}
|
|
12842
|
+
function shopifyWorkspaceResultKey(command) {
|
|
12843
|
+
return command === 'shopify workspace deploy-source' ? 'deployment' : 'validation';
|
|
12844
|
+
}
|
|
12845
|
+
function shopifyWorkspaceCommandSucceeded(command, payload) {
|
|
12846
|
+
if (payload.ok !== true) {
|
|
12847
|
+
return false;
|
|
12848
|
+
}
|
|
12849
|
+
const result = payload[shopifyWorkspaceResultKey(command.command)];
|
|
12850
|
+
if (result && typeof result === 'object' && !Array.isArray(result)) {
|
|
12851
|
+
const ok = result.ok;
|
|
12852
|
+
return ok !== false;
|
|
12853
|
+
}
|
|
12854
|
+
return true;
|
|
12855
|
+
}
|
|
12856
|
+
function shopifyWorkspaceTextLines(command, payload) {
|
|
12857
|
+
const connector = payload.connector && typeof payload.connector === 'object' && !Array.isArray(payload.connector)
|
|
12858
|
+
? payload.connector
|
|
12859
|
+
: {};
|
|
12860
|
+
const source = payload.source && typeof payload.source === 'object' && !Array.isArray(payload.source)
|
|
12861
|
+
? payload.source
|
|
12862
|
+
: {};
|
|
12863
|
+
const result = payload[shopifyWorkspaceResultKey(command.command)];
|
|
12864
|
+
const resultRecord = result && typeof result === 'object' && !Array.isArray(result)
|
|
12865
|
+
? result
|
|
12866
|
+
: {};
|
|
12867
|
+
const succeeded = shopifyWorkspaceCommandSucceeded(command, payload);
|
|
12868
|
+
const verb = command.command === 'shopify workspace deploy-source' ? 'deploy' : 'validate';
|
|
12869
|
+
return [
|
|
12870
|
+
`${verb}: ${succeeded ? 'completed' : 'failed'}`,
|
|
12871
|
+
`connector_id: ${String(connector.connectorId ?? '')}`,
|
|
12872
|
+
`source_artifact_id: ${String(source.sourceArtifactId ?? '')}`,
|
|
12873
|
+
...(payload.reasonCode ? [`reason: ${String(payload.reasonCode)}`] : []),
|
|
12874
|
+
...(resultRecord.exitCode !== undefined ? [`exit_code: ${String(resultRecord.exitCode)}`] : []),
|
|
12875
|
+
...(resultRecord.stderr ? [`stderr: ${String(resultRecord.stderr).trim()}`] : []),
|
|
12876
|
+
].filter((line) => !line.endsWith(': '));
|
|
12877
|
+
}
|
|
12878
|
+
function readShopifyWorkspaceRuntimeIdentity(runtime) {
|
|
12879
|
+
const env = runtime.env ?? process.env;
|
|
12880
|
+
const baseUrl = normalizeHostedWorkspaceBaseUrl(env.TENDER_BASE_URL);
|
|
12881
|
+
if (!baseUrl) {
|
|
12882
|
+
throw new TenderCliUsageError('TENDER_BASE_URL is required. Open or reload the hosted Shopify workspace and run this command inside its terminal.');
|
|
12883
|
+
}
|
|
12884
|
+
if (!isHostedWorkspaceInternalApiBaseUrl(baseUrl)) {
|
|
12885
|
+
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.');
|
|
12886
|
+
}
|
|
12887
|
+
const runtimeArtifactId = readHostedWorkspaceArtifactId(env.TENDER_RUNTIME_ARTIFACT_ID);
|
|
12888
|
+
if (!runtimeArtifactId) {
|
|
12889
|
+
throw new TenderCliUsageError('TENDER_RUNTIME_ARTIFACT_ID is required. This command only runs inside a hosted Shopify project workspace.');
|
|
12890
|
+
}
|
|
12891
|
+
return { baseUrl, runtimeArtifactId };
|
|
12892
|
+
}
|
|
12893
|
+
function readShopifyWorkspaceConnectorRecords(payload) {
|
|
12894
|
+
return Array.isArray(payload.connectors)
|
|
12895
|
+
? payload.connectors.filter((connector) => Boolean(connector) && typeof connector === 'object' && !Array.isArray(connector))
|
|
12896
|
+
: [];
|
|
12897
|
+
}
|
|
12898
|
+
function readShopifyWorkspaceConnectorSourceArtifactId(connector) {
|
|
12899
|
+
const sourceProject = connector.sourceProject && typeof connector.sourceProject === 'object' && !Array.isArray(connector.sourceProject)
|
|
12900
|
+
? connector.sourceProject
|
|
12901
|
+
: null;
|
|
12902
|
+
return readHostedWorkspaceArtifactId(typeof sourceProject?.sourceArtifactId === 'string' ? sourceProject.sourceArtifactId : undefined);
|
|
12903
|
+
}
|
|
12904
|
+
function resolveShopifyWorkspaceSourceCommandConnector(input) {
|
|
12905
|
+
const connectors = readShopifyWorkspaceConnectorRecords(input.payload);
|
|
12906
|
+
const connector = input.selectedConnectorId
|
|
12907
|
+
? connectors.find((candidate) => candidate.connectorId === input.selectedConnectorId) ?? null
|
|
12908
|
+
: (() => {
|
|
12909
|
+
const linked = connectors.filter((candidate) => candidate.linked === true);
|
|
12910
|
+
return linked.length === 1 ? linked[0] ?? null : null;
|
|
12911
|
+
})();
|
|
12912
|
+
if (!connector) {
|
|
12913
|
+
return {
|
|
12914
|
+
ok: false,
|
|
12915
|
+
payload: {
|
|
12916
|
+
ok: false,
|
|
12917
|
+
reasonCode: input.selectedConnectorId
|
|
12918
|
+
? 'shopify_workspace_selected_connector_not_found'
|
|
12919
|
+
: 'shopify_workspace_connector_selection_required',
|
|
12920
|
+
message: input.selectedConnectorId
|
|
12921
|
+
? 'The selected Shopify connector is no longer available to this workspace.'
|
|
12922
|
+
: 'Link this runtime to one Shopify connector before validating or deploying source.',
|
|
12923
|
+
},
|
|
12924
|
+
};
|
|
12925
|
+
}
|
|
12926
|
+
const connectorId = typeof connector.connectorId === 'string' ? connector.connectorId : null;
|
|
12927
|
+
const sourceArtifactId = readShopifyWorkspaceConnectorSourceArtifactId(connector);
|
|
12928
|
+
if (!connectorId || !sourceArtifactId) {
|
|
12929
|
+
return {
|
|
12930
|
+
ok: false,
|
|
12931
|
+
payload: {
|
|
12932
|
+
ok: false,
|
|
12933
|
+
reasonCode: 'shopify_source_project_required',
|
|
12934
|
+
message: 'The selected Shopify connector does not have a source project.',
|
|
12935
|
+
},
|
|
12936
|
+
};
|
|
12937
|
+
}
|
|
12938
|
+
return {
|
|
12939
|
+
ok: true,
|
|
12940
|
+
connectorId,
|
|
12941
|
+
sourceArtifactId,
|
|
12942
|
+
};
|
|
12943
|
+
}
|
|
12944
|
+
async function runShopifyWorkspaceSourceCommand(command, io, runtime) {
|
|
12945
|
+
const { baseUrl, runtimeArtifactId } = readShopifyWorkspaceRuntimeIdentity(runtime);
|
|
12946
|
+
const selectedConnectorId = await readHostedWorkspaceConnectorSelection(runtimeArtifactId, runtime);
|
|
12947
|
+
const connectorsResult = await requestShopifyWorkspaceConnectorsList({
|
|
12948
|
+
baseUrl,
|
|
12949
|
+
runtimeArtifactId,
|
|
12950
|
+
fetcher: runtime.fetcher ?? fetch,
|
|
12951
|
+
});
|
|
12952
|
+
if (!shopifyWorkspaceRequestSucceeded(connectorsResult)) {
|
|
12953
|
+
const payload = {
|
|
12954
|
+
...connectorsResult.payload,
|
|
12955
|
+
command: command.command,
|
|
12956
|
+
runtimeArtifactId,
|
|
12957
|
+
...(selectedConnectorId ? { selectedConnectorId } : {}),
|
|
12958
|
+
};
|
|
12959
|
+
if (command.json) {
|
|
12960
|
+
io.stdout(JSON.stringify(payload, null, 2));
|
|
12961
|
+
}
|
|
12962
|
+
else {
|
|
12963
|
+
io.stdout(shopifyWorkspaceTextLines(command, payload).join('\n'));
|
|
12964
|
+
}
|
|
12965
|
+
return 1;
|
|
12966
|
+
}
|
|
12967
|
+
const resolvedConnector = resolveShopifyWorkspaceSourceCommandConnector({
|
|
12968
|
+
payload: connectorsResult.payload,
|
|
12969
|
+
selectedConnectorId,
|
|
12970
|
+
});
|
|
12971
|
+
if (!resolvedConnector.ok) {
|
|
12972
|
+
const payload = {
|
|
12973
|
+
...resolvedConnector.payload,
|
|
12974
|
+
command: command.command,
|
|
12975
|
+
runtimeArtifactId,
|
|
12976
|
+
...(selectedConnectorId ? { selectedConnectorId } : {}),
|
|
12977
|
+
};
|
|
12978
|
+
if (command.json) {
|
|
12979
|
+
io.stdout(JSON.stringify(payload, null, 2));
|
|
12980
|
+
}
|
|
12981
|
+
else {
|
|
12982
|
+
io.stdout(shopifyWorkspaceTextLines(command, payload).join('\n'));
|
|
12983
|
+
}
|
|
12984
|
+
return 1;
|
|
12985
|
+
}
|
|
12986
|
+
const result = await requestShopifyWorkspaceSourceCommand({
|
|
12987
|
+
command: command.command,
|
|
12988
|
+
baseUrl,
|
|
12989
|
+
runtimeArtifactId,
|
|
12990
|
+
fetcher: runtime.fetcher ?? fetch,
|
|
12991
|
+
connectorId: resolvedConnector.connectorId,
|
|
12992
|
+
sourceArtifactId: resolvedConnector.sourceArtifactId,
|
|
12993
|
+
});
|
|
12994
|
+
const payload = {
|
|
12995
|
+
...result.payload,
|
|
12996
|
+
command: command.command,
|
|
12997
|
+
runtimeArtifactId,
|
|
12998
|
+
selectedConnectorId: resolvedConnector.connectorId,
|
|
12999
|
+
};
|
|
13000
|
+
if (command.json) {
|
|
13001
|
+
io.stdout(JSON.stringify(payload, null, 2));
|
|
13002
|
+
}
|
|
13003
|
+
else {
|
|
13004
|
+
io.stdout(shopifyWorkspaceTextLines(command, payload).join('\n'));
|
|
13005
|
+
}
|
|
13006
|
+
return shopifyWorkspaceCommandSucceeded(command, payload) ? 0 : 1;
|
|
13007
|
+
}
|
|
13008
|
+
function shopifyWorkspaceRequestSucceeded(result) {
|
|
13009
|
+
return result.status >= 200 && result.status < 300 && result.payload.ok !== false;
|
|
13010
|
+
}
|
|
13011
|
+
function shopifyWorkspaceConnectorListTextLines(payload) {
|
|
13012
|
+
const connectors = readShopifyWorkspaceConnectorRecords(payload);
|
|
13013
|
+
const lines = [
|
|
13014
|
+
`runtime_artifact_id: ${String(payload.runtimeArtifactId ?? '')}`,
|
|
13015
|
+
`shop_domain: ${String(payload.shopDomain ?? '')}`,
|
|
13016
|
+
`connectors: ${connectors.length}`,
|
|
13017
|
+
];
|
|
13018
|
+
for (const connector of connectors) {
|
|
13019
|
+
lines.push([
|
|
13020
|
+
String(connector.connectorId ?? ''),
|
|
13021
|
+
String(connector.label ?? connector.appName ?? ''),
|
|
13022
|
+
connector.linked ? 'linked' : 'not_linked',
|
|
13023
|
+
connector.ready ? 'ready' : 'needs_setup',
|
|
13024
|
+
]
|
|
13025
|
+
.filter(Boolean)
|
|
13026
|
+
.join(' | '));
|
|
13027
|
+
}
|
|
13028
|
+
if (typeof payload.reasonCode === 'string') {
|
|
13029
|
+
lines.push(`reason: ${payload.reasonCode}`);
|
|
13030
|
+
}
|
|
13031
|
+
return lines.filter((line) => !line.endsWith(': '));
|
|
13032
|
+
}
|
|
13033
|
+
function shopifyWorkspaceLinkRuntimeTextLines(payload) {
|
|
13034
|
+
const connector = payload.connector && typeof payload.connector === 'object' && !Array.isArray(payload.connector)
|
|
13035
|
+
? payload.connector
|
|
13036
|
+
: {};
|
|
13037
|
+
const source = payload.source && typeof payload.source === 'object' && !Array.isArray(payload.source)
|
|
13038
|
+
? payload.source
|
|
13039
|
+
: {};
|
|
13040
|
+
const workspace = payload.workspace && typeof payload.workspace === 'object' && !Array.isArray(payload.workspace)
|
|
13041
|
+
? payload.workspace
|
|
13042
|
+
: {};
|
|
13043
|
+
return [
|
|
13044
|
+
`link: ${payload.ok === false ? 'failed' : 'created'}`,
|
|
13045
|
+
`runtime_artifact_id: ${String(payload.runtimeArtifactId ?? '')}`,
|
|
13046
|
+
`connector_id: ${String(connector.connectorId ?? '')}`,
|
|
13047
|
+
`source_artifact_id: ${String(source.sourceArtifactId ?? '')}`,
|
|
13048
|
+
workspace.reloadRequired ? 'next: reload the hosted workspace to mount /workspace/shopify-connector' : '',
|
|
13049
|
+
...(payload.reasonCode ? [`reason: ${String(payload.reasonCode)}`] : []),
|
|
13050
|
+
].filter(Boolean);
|
|
13051
|
+
}
|
|
13052
|
+
async function runShopifyWorkspaceConnectorsList(command, io, runtime) {
|
|
13053
|
+
const { baseUrl, runtimeArtifactId } = readShopifyWorkspaceRuntimeIdentity(runtime);
|
|
13054
|
+
const result = await requestShopifyWorkspaceConnectorsList({
|
|
13055
|
+
baseUrl,
|
|
13056
|
+
runtimeArtifactId,
|
|
13057
|
+
fetcher: runtime.fetcher ?? fetch,
|
|
13058
|
+
});
|
|
13059
|
+
const payload = {
|
|
13060
|
+
...result.payload,
|
|
13061
|
+
command: command.command,
|
|
13062
|
+
runtimeArtifactId,
|
|
13063
|
+
};
|
|
13064
|
+
if (command.json) {
|
|
13065
|
+
io.stdout(JSON.stringify(payload, null, 2));
|
|
13066
|
+
}
|
|
13067
|
+
else {
|
|
13068
|
+
io.stdout(shopifyWorkspaceConnectorListTextLines(payload).join('\n'));
|
|
13069
|
+
}
|
|
13070
|
+
return shopifyWorkspaceRequestSucceeded(result) ? 0 : 1;
|
|
13071
|
+
}
|
|
13072
|
+
async function runShopifyWorkspaceLinkRuntime(command, io, runtime) {
|
|
13073
|
+
const { baseUrl, runtimeArtifactId } = readShopifyWorkspaceRuntimeIdentity(runtime);
|
|
13074
|
+
const result = await requestShopifyWorkspaceLinkRuntime({
|
|
13075
|
+
baseUrl,
|
|
13076
|
+
runtimeArtifactId,
|
|
13077
|
+
connectorId: command.connectorId,
|
|
13078
|
+
fetcher: runtime.fetcher ?? fetch,
|
|
13079
|
+
});
|
|
13080
|
+
if (shopifyWorkspaceRequestSucceeded(result)) {
|
|
13081
|
+
await writeHostedWorkspaceConnectorSelection({
|
|
13082
|
+
runtimeArtifactId,
|
|
13083
|
+
connectorId: command.connectorId,
|
|
13084
|
+
}, runtime);
|
|
13085
|
+
}
|
|
13086
|
+
const payload = {
|
|
13087
|
+
...result.payload,
|
|
13088
|
+
command: command.command,
|
|
13089
|
+
runtimeArtifactId,
|
|
13090
|
+
connectorId: command.connectorId,
|
|
13091
|
+
};
|
|
13092
|
+
if (command.json) {
|
|
13093
|
+
io.stdout(JSON.stringify(payload, null, 2));
|
|
13094
|
+
}
|
|
13095
|
+
else {
|
|
13096
|
+
io.stdout(shopifyWorkspaceLinkRuntimeTextLines(payload).join('\n'));
|
|
13097
|
+
}
|
|
13098
|
+
return shopifyWorkspaceRequestSucceeded(result) ? 0 : 1;
|
|
13099
|
+
}
|
|
12061
13100
|
async function runConnectorsShopifyDelete(command, io, runtime) {
|
|
12062
13101
|
if (command.confirm !== 'delete') {
|
|
12063
13102
|
throw new TenderCliUsageError('--confirm delete is required. Example: tender connectors shopify delete conn_123 --confirm delete --json');
|
|
@@ -12897,6 +13936,9 @@ function cliErrorMessage(error) {
|
|
|
12897
13936
|
return String(error);
|
|
12898
13937
|
}
|
|
12899
13938
|
function cliErrorCode(error) {
|
|
13939
|
+
if (error instanceof TenderCliChildProcessError) {
|
|
13940
|
+
return 'child_process_failed';
|
|
13941
|
+
}
|
|
12900
13942
|
if (error instanceof TenderCliDbApiError || error instanceof TenderCliAnalyticsApiError || error instanceof TenderCliConnectorsApiError) {
|
|
12901
13943
|
return error.reasonCode;
|
|
12902
13944
|
}
|
|
@@ -12966,6 +14008,9 @@ function exampleForError(error, args, command) {
|
|
|
12966
14008
|
if (error instanceof TenderCliDbApiError || error instanceof TenderCliAnalyticsApiError) {
|
|
12967
14009
|
return error.example;
|
|
12968
14010
|
}
|
|
14011
|
+
if (error instanceof TenderCliConnectorsApiError) {
|
|
14012
|
+
return error.example;
|
|
14013
|
+
}
|
|
12969
14014
|
const commandName = commandNameForError(args, command);
|
|
12970
14015
|
const dbPath = dbPathFromCommandName(commandName);
|
|
12971
14016
|
const artifactId = artifactIdForError(args, command) ?? 'artifact_123';
|
|
@@ -12981,6 +14026,16 @@ function cliErrorPayload(error, args, command) {
|
|
|
12981
14026
|
const scope = scopeForError(error, args, command);
|
|
12982
14027
|
const example = exampleForError(error, args, command);
|
|
12983
14028
|
const nextAction = nextActionForError(error);
|
|
14029
|
+
const childProcess = error instanceof TenderCliChildProcessError
|
|
14030
|
+
? {
|
|
14031
|
+
command: error.command,
|
|
14032
|
+
args: error.args,
|
|
14033
|
+
commandLine: commandLine(error.command, error.args),
|
|
14034
|
+
exitCode: error.exitCode,
|
|
14035
|
+
stdout: error.stdout,
|
|
14036
|
+
stderr: error.stderr,
|
|
14037
|
+
}
|
|
14038
|
+
: null;
|
|
12984
14039
|
return {
|
|
12985
14040
|
ok: false,
|
|
12986
14041
|
command: commandNameForError(args, command),
|
|
@@ -12989,6 +14044,7 @@ function cliErrorPayload(error, args, command) {
|
|
|
12989
14044
|
...(scope ? { scope } : {}),
|
|
12990
14045
|
...(nextAction ? { nextAction } : {}),
|
|
12991
14046
|
...(example ? { example } : {}),
|
|
14047
|
+
...(childProcess ? { childProcess } : {}),
|
|
12992
14048
|
};
|
|
12993
14049
|
}
|
|
12994
14050
|
export async function runTenderCli(args, io = {
|
|
@@ -13092,6 +14148,14 @@ export async function runTenderCli(args, io = {
|
|
|
13092
14148
|
else if (command.topic === 'connectors bind') {
|
|
13093
14149
|
io.stdout(connectorsBindHelp());
|
|
13094
14150
|
}
|
|
14151
|
+
else if (command.topic === 'shopify' ||
|
|
14152
|
+
command.topic === 'shopify workspace' ||
|
|
14153
|
+
command.topic === 'shopify workspace connectors list' ||
|
|
14154
|
+
command.topic === 'shopify workspace link-runtime' ||
|
|
14155
|
+
command.topic === 'shopify workspace validate-source' ||
|
|
14156
|
+
command.topic === 'shopify workspace deploy-source') {
|
|
14157
|
+
io.stdout(shopifyWorkspaceHelp());
|
|
14158
|
+
}
|
|
13095
14159
|
else if (command.topic === 'auth') {
|
|
13096
14160
|
io.stdout(authHelp());
|
|
13097
14161
|
}
|
|
@@ -13352,6 +14416,15 @@ export async function runTenderCli(args, io = {
|
|
|
13352
14416
|
if (command.command === 'connectors shopify source deploy') {
|
|
13353
14417
|
return await runConnectorsShopifySourceDeploy(command, io, runtime);
|
|
13354
14418
|
}
|
|
14419
|
+
if (command.command === 'shopify workspace validate-source' || command.command === 'shopify workspace deploy-source') {
|
|
14420
|
+
return await runShopifyWorkspaceSourceCommand(command, io, runtime);
|
|
14421
|
+
}
|
|
14422
|
+
if (command.command === 'shopify workspace connectors list') {
|
|
14423
|
+
return await runShopifyWorkspaceConnectorsList(command, io, runtime);
|
|
14424
|
+
}
|
|
14425
|
+
if (command.command === 'shopify workspace link-runtime') {
|
|
14426
|
+
return await runShopifyWorkspaceLinkRuntime(command, io, runtime);
|
|
14427
|
+
}
|
|
13355
14428
|
if (command.command === 'connectors shopify delete') {
|
|
13356
14429
|
return await runConnectorsShopifyDelete(command, io, runtime);
|
|
13357
14430
|
}
|
|
@@ -13524,7 +14597,7 @@ export async function runTenderCli(args, io = {
|
|
|
13524
14597
|
}
|
|
13525
14598
|
catch (error) {
|
|
13526
14599
|
if (args.includes('--json') &&
|
|
13527
|
-
((args[0] === 'app' && (args[1] === 'db' || (args[1] === 'agent' && args[2] === 'heartbeat'))) || args[0] === 'playbooks' || args[0] === 'connectors')) {
|
|
14600
|
+
((args[0] === 'app' && (args[1] === 'db' || (args[1] === 'agent' && args[2] === 'heartbeat'))) || args[0] === 'playbooks' || args[0] === 'connectors' || args[0] === 'shopify')) {
|
|
13528
14601
|
io.stdout(JSON.stringify(cliErrorPayload(error, args, command), null, 2));
|
|
13529
14602
|
return error instanceof TenderCliUsageError ? 2 : 1;
|
|
13530
14603
|
}
|