@tenderprompt/cli 0.1.32 → 0.1.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +28 -5
  2. package/dist/index.js +1327 -162
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -31,6 +31,7 @@ function readCliPackageVersion() {
31
31
  return UNKNOWN_CLI_VERSION;
32
32
  }
33
33
  const CLI_PACKAGE_VERSION = readCliPackageVersion();
34
+ const GIT_COMMIT_SHA_PATTERN = /^[0-9a-f]{40}$/i;
34
35
  const ANALYTICS_AUTHORING_PLAYBOOK_ID = 'analytics-event-authoring';
35
36
  const ANALYTICS_AUTHORING_COMMAND = 'tender app analytics authoring --json';
36
37
  const ANALYTICS_AUTHORING_PLAYBOOK_COMMAND = `tender playbooks get ${ANALYTICS_AUTHORING_PLAYBOOK_ID} --json`;
@@ -89,6 +90,7 @@ const ANALYTICS_AGENT_GUIDANCE = {
89
90
  'clear split between shopper-facing UI and operator-facing analytics',
90
91
  ],
91
92
  };
93
+ const DEFAULT_TENDER_APP_SCAFFOLD_TEMPLATE = 'server-backed';
92
94
  class TenderCliUsageError extends Error {
93
95
  constructor(message) {
94
96
  super(message);
@@ -133,12 +135,14 @@ class TenderCliConnectorsApiError extends Error {
133
135
  reasonCode;
134
136
  status;
135
137
  payload;
136
- constructor(message, reasonCode, status, payload = null) {
138
+ example;
139
+ constructor(message, reasonCode, status, payload = null, example = 'tender connectors list --profile account --json') {
137
140
  super(message);
138
141
  this.name = 'TenderCliConnectorsApiError';
139
142
  this.reasonCode = reasonCode;
140
143
  this.status = status;
141
144
  this.payload = payload;
145
+ this.example = example;
142
146
  }
143
147
  }
144
148
  const DEFAULT_PROFILE_NAME = 'default';
@@ -176,6 +180,12 @@ Commands:
176
180
  tender connectors shopify verify
177
181
  Exchange credentials for an Admin API access token
178
182
  tender connectors bind Print an app.json binding for a connector instance
183
+ tender shopify workspace connectors list
184
+ List current-store Shopify connectors from a hosted workspace
185
+ tender shopify workspace link-runtime --connector <connector-id>
186
+ Link the hosted runtime to a Shopify connector
187
+ tender shopify workspace deploy-source
188
+ Deploy linked Shopify source from a hosted workspace
179
189
  tender app list List Tender App records visible to the current token
180
190
  tender app create --name <name>
181
191
  Create a new Tender App in the current account
@@ -238,8 +248,8 @@ Commands:
238
248
  Examples:
239
249
  tender --version
240
250
  tender capabilities --json
241
- tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d
242
- tender auth status --json
251
+ tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d --save-profile agent --json
252
+ tender auth status --profile agent --json
243
253
  tender memory add --kind decision --summary "Kept auth in the host gateway" --json
244
254
  tender memory handoff --reason "Preview publish is blocked" --summary "Need Tender support" --json
245
255
  tender memory brief --json
@@ -253,13 +263,18 @@ Examples:
253
263
  printf "$SHOPIFY_APP_AUTOMATION_TOKEN" | tender connectors shopify app-automation-token set conn_123 --token-stdin --expires-at 2026-08-22 --profile account --json
254
264
  tender connectors shopify verify conn_123 --profile account --json
255
265
  tender connectors bind SHOPIFY_CONNECTOR conn_123 --instance default --json
266
+ tender shopify workspace connectors list --json
267
+ tender shopify workspace link-runtime --connector conn_123 --json
268
+ tender shopify workspace deploy-source --json
256
269
  tender app list --json
257
270
  tender app create --name "Exit Intent Widget" --json
258
- tender app create --name "Exit Intent Widget" --init --dir ./widget --preview --json
271
+ tender app create --name "Data Collection App" --init --dir ./data-collection-app --preview --json
272
+ tender app create --name "Product Page Survey Runtime" --init --dir ./product-survey-runtime --scaffold shopify-pdp-survey-runtime --preview --json
259
273
  tender app update artifact_123 --name "Sale Widget" --json
260
274
  tender app share create artifact_123 --expires 7d --claims 1 --json
261
275
  tender app claim tok_share_123 --dir ./widget --confirm source-and-setup --json
262
- tender app init artifact_123 --dir ./widget --preview --json
276
+ tender app init artifact_123 --dir ./data-collection-app --preview --json
277
+ tender app init artifact_123 --dir ./product-survey-runtime --scaffold shopify-pdp-survey-runtime --preview --json
263
278
  tender app validate artifact_123 --json
264
279
  tender app build artifact_123 --commit $(git rev-parse HEAD) --json
265
280
  tender app preview artifact_123 --commit $(git rev-parse HEAD) --stream --json
@@ -294,12 +309,13 @@ Commands:
294
309
 
295
310
  Examples:
296
311
  tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d
297
- tender auth login --device --profile publish --artifact artifact_123 --ttl 7d --save-profile publish
312
+ tender auth login --device --profile publish --artifact artifact_123 --ttl 7d --save-profile publish --json
313
+ tender auth status --profile publish --json
298
314
  tender auth login --device --profile create --shopify-store tender-prompt --json
299
315
  tender auth status --json`;
300
316
  }
301
317
  function authLoginHelp() {
302
- return `Usage: tender auth login --device [--base-url <url>] [--shopify-store <store>] [--shopify-app-url <url>] [--profile <edit-preview|publish|create|db-read>] [--artifact <artifact-id>] [--ttl <7d|14d|30d>] [--save-profile <name>] [--no-poll] [--json]
318
+ return `Usage: tender auth login --device [--base-url <url>] [--shopify-store <store>] [--shopify-app-url <url>] [--profile <edit-preview|publish|create|db-read>] [--artifact <artifact-id>] [--ttl <7d|14d|30d>] [--save-profile <name>] [--no-poll|--poll] [--json]
303
319
 
304
320
  Options:
305
321
  --device Use the OAuth device-code flow.
@@ -310,12 +326,14 @@ Options:
310
326
  --artifact <artifact-id> Scope the token to one artifact. Required for normal edit-preview agent work.
311
327
  --ttl <value> Token lifetime: 7d, 14d, or 30d. Defaults to 7d.
312
328
  --save-profile <name> Local config profile name. Defaults to default, or shopify-<store> with Shopify approval.
313
- --no-poll Print the verification URL and exit without waiting for approval.
314
- --json Emit stable JSON for coding agents.
329
+ --no-poll Print the verification URL, store the pending device auth, and exit without waiting.
330
+ --poll With --json, wait for approval and save the profile after printing the pending handoff to stderr.
331
+ --json Emit stable JSON for coding agents. Defaults to --no-poll unless --poll is provided.
315
332
 
316
333
  Examples:
317
334
  tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d
318
- tender auth login --device --profile publish --artifact artifact_123 --ttl 7d --save-profile publish --json
335
+ tender auth login --device --profile publish --artifact artifact_123 --ttl 7d --save-profile publish --no-poll --json
336
+ tender auth status --profile publish --json
319
337
  tender auth login --device --profile create --ttl 7d --save-profile account
320
338
  tender auth login --device --profile create --shopify-store tender-prompt --json
321
339
  tender auth login --device --profile db-read --artifact artifact_123 --ttl 7d --save-profile db`;
@@ -460,7 +478,34 @@ Examples:
460
478
  tender connectors shopify source init conn_123 --dir ./shopify-app --client-id abc --profile account --json
461
479
  tender connectors shopify source validate conn_123 --dir ./shopify-app --execute --json
462
480
  tender connectors shopify source deploy conn_123 --dir ./shopify-app --confirm deploy --profile account --json
463
- tender connectors bind SHOPIFY_CONNECTOR conn_123 --instance default --json`;
481
+ tender connectors bind SHOPIFY_CONNECTOR conn_123 --instance default --json`;
482
+ }
483
+ function shopifyWorkspaceHelp() {
484
+ return `Usage: tender shopify workspace <command> [--json]
485
+
486
+ Hosted Shopify workspace commands use the current terminal workspace identity.
487
+ They do not read Tender profiles, account-scoped tokens, Shopify client secrets,
488
+ or Shopify App Automation Tokens from the terminal.
489
+
490
+ Commands:
491
+ tender shopify workspace connectors list
492
+ List current-store Shopify connectors available to this runtime
493
+ tender shopify workspace link-runtime --connector <connector-id>
494
+ Link this runtime to a Shopify connector source project
495
+ tender shopify workspace validate-source
496
+ Validate the linked Shopify connector source
497
+ tender shopify workspace deploy-source
498
+ Deploy the linked Shopify connector source
499
+
500
+ Required environment:
501
+ TENDER_BASE_URL
502
+ TENDER_RUNTIME_ARTIFACT_ID
503
+
504
+ Examples:
505
+ tender shopify workspace connectors list --json
506
+ tender shopify workspace link-runtime --connector conn_123 --json
507
+ tender shopify workspace validate-source --json
508
+ tender shopify workspace deploy-source --json`;
464
509
  }
465
510
  function connectorsShopifyCreateHelp() {
466
511
  return `Usage: tender connectors shopify create [--instance <id>] [--label <text>] [--app-name <text>] [--admin-api-version <version>] [--shop <myshopify-domain>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
@@ -517,19 +562,22 @@ Examples:
517
562
  tender connectors shopify verify conn_123 --profile account --json`;
518
563
  }
519
564
  function connectorsShopifySourceInitHelp() {
520
- return `Usage: tender connectors shopify source init <connector-id> --dir <path> --client-id <shopify-client-id> [--source-name <name>] [--scopes <csv>] [--force] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
565
+ return `Usage: tender connectors shopify source init <connector-id> --dir <path> --client-id <shopify-client-id> [--template <minimal|shopify-pdp-survey>] [--source-name <name>] [--scopes <csv>] [--force] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
521
566
 
522
567
  Creates or reuses an internal Tender source artifact for the merchant-owned
523
- Shopify app, checks it out to --dir, writes a minimal non-secret Shopify app
524
- source tree, commits it, pushes it to Artifact Git, and records
568
+ Shopify app, checks it out to --dir, writes a product page survey app block
569
+ source tree by default, commits it, pushes it to Artifact Git, and records
525
570
  sourceArtifactId on the connector.
526
571
 
527
- The CLI bootstraps and deploys source. App blocks, web pixels, customer
528
- metadata definitions, and app-owned GraphQL resources are source edits guided by
529
- playbooks, not source init flags.
572
+ Pass --template minimal for an empty non-secret Shopify app source tree.
573
+
574
+ The CLI bootstraps source. Additional app blocks, web pixels, customer metadata
575
+ definitions, and app-owned GraphQL resources are source edits guided by
576
+ playbooks.
530
577
 
531
578
  Examples:
532
- tender connectors shopify source init conn_123 --dir ./shopify-app --client-id abc --profile account --json`;
579
+ tender connectors shopify source init conn_123 --dir ./shopify-app --client-id abc --profile account --json
580
+ tender connectors shopify source init conn_123 --dir ./shopify-app --client-id abc --template minimal --profile account --json`;
533
581
  }
534
582
  function connectorsShopifySourceCheckoutHelp() {
535
583
  return `Usage: tender connectors shopify source checkout <connector-id> --dir <path> [--remote <name>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
@@ -558,6 +606,13 @@ SHOPIFY_APP_AUTOMATION_TOKEN is not set locally, the CLI requests the
558
606
  connector's stored App Automation Token and injects it only into the spawned
559
607
  Shopify CLI child process.
560
608
 
609
+ Inside hosted Shopify project workspaces, direct shopify app deploy is allowed
610
+ when Shopify CLI is already authenticated there. Otherwise use
611
+ tender shopify workspace deploy-source so Tender can use the connector's
612
+ stored App Automation Token server-side. Do not run this account-scoped command
613
+ with a hosted workspace artifact token. Never ask the merchant to paste token
614
+ material into the terminal.
615
+
561
616
  Examples:
562
617
  tender connectors shopify source deploy conn_123 --dir ./shopify-app --version connector-source-smoke --confirm deploy --profile account --json`;
563
618
  }
@@ -676,12 +731,14 @@ Commands:
676
731
  Examples:
677
732
  tender app list --json
678
733
  tender app create --name "Exit Intent Widget" --json
679
- tender app create --name "Exit Intent Widget" --init --dir ./widget --preview --json
734
+ tender app create --name "Data Collection App" --init --dir ./data-collection-app --preview --json
735
+ tender app create --name "Product Page Survey Runtime" --init --dir ./product-survey-runtime --scaffold shopify-pdp-survey-runtime --preview --json
680
736
  tender app update artifact_123 --name "Sale Widget" --json
681
737
  tender app share create artifact_123 --expires 7d --claims 1 --json
682
738
  tender app share inspect tok_share_123 --json
683
739
  tender app claim tok_share_123 --dir ./widget --confirm source-and-setup --json
684
- tender app init artifact_123 --dir ./widget --preview --json
740
+ tender app init artifact_123 --dir ./data-collection-app --preview --json
741
+ tender app init artifact_123 --dir ./product-survey-runtime --scaffold shopify-pdp-survey-runtime --preview --json
685
742
  tender app validate artifact_123 --json
686
743
  tender app build artifact_123 --commit $(git rev-parse HEAD) --json
687
744
  tender app preview artifact_123 --commit $(git rev-parse HEAD) --stream --json
@@ -1105,13 +1162,13 @@ Examples:
1105
1162
  TENDER_API_TOKEN=tpat_... tender app list --base-url https://app.tenderprompt.com --json`;
1106
1163
  }
1107
1164
  function artifactsCreateHelp() {
1108
- return `Usage: tender app create --name <name> [--init] [--dir <path>] [--scaffold <server-backed|none>] [--preview] [--target-pack <id>] [--target-pack-version <version>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
1165
+ return `Usage: tender app create --name <name> [--init] [--dir <path>] [--scaffold <server-backed|shopify-pdp-survey-runtime|none>] [--preview] [--target-pack <id>] [--target-pack-version <version>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
1109
1166
 
1110
1167
  Options:
1111
1168
  --name <name> App name.
1112
1169
  --init Initialize a local checkout after creating the app.
1113
1170
  --dir <path> Local checkout directory for --init. Defaults to current directory.
1114
- --scaffold <value> API-derived scaffold for empty app source: server-backed or none. Defaults to server-backed.
1171
+ --scaffold <value> API-derived scaffold for empty app source: server-backed, shopify-pdp-survey-runtime, or none. Defaults to server-backed.
1115
1172
  --no-scaffold Alias for --scaffold none.
1116
1173
  --preview After --init writes and pushes initial source, request a preview build.
1117
1174
  --remote <name> Git remote name for --init. Defaults to tender.
@@ -1126,7 +1183,8 @@ Options:
1126
1183
 
1127
1184
  Examples:
1128
1185
  tender app create --name "Exit Intent Widget" --json
1129
- tender app create --name "Exit Intent Widget" --init --dir ./widget --preview --json
1186
+ tender app create --name "Data Collection App" --init --dir ./data-collection-app --preview --json
1187
+ tender app create --name "Product Page Survey Runtime" --init --dir ./product-survey-runtime --scaffold shopify-pdp-survey-runtime --preview --json
1130
1188
  tender app create --name "Exit Intent Widget" --profile account --json`;
1131
1189
  }
1132
1190
  function artifactsUpdateHelp() {
@@ -1143,12 +1201,12 @@ Examples:
1143
1201
  tender app update artifact_123 --name "Sale Widget" --json`;
1144
1202
  }
1145
1203
  function artifactsInitHelp() {
1146
- return `Usage: tender app init <app-id> [--dir <path>] [--remote <name>] [--scaffold <server-backed|none>] [--preview] [--base-url <url>] [--token <token>] [--profile <name>] [--ttl <7d|14d|30d>] [--dry-run] [--force] [--json]
1204
+ return `Usage: tender app init <app-id> [--dir <path>] [--remote <name>] [--scaffold <server-backed|shopify-pdp-survey-runtime|none>] [--preview] [--base-url <url>] [--token <token>] [--profile <name>] [--ttl <7d|14d|30d>] [--dry-run] [--force] [--json]
1147
1205
 
1148
1206
  Options:
1149
1207
  --dir <path> Local checkout directory to create/update. Defaults to current directory.
1150
1208
  --remote <name> Git remote name to add or update. Defaults to tender.
1151
- --scaffold <value> API-derived scaffold for empty app source: server-backed or none. Defaults to server-backed.
1209
+ --scaffold <value> API-derived scaffold for empty app source: server-backed, shopify-pdp-survey-runtime, or none. Defaults to server-backed.
1152
1210
  --no-scaffold Alias for --scaffold none.
1153
1211
  --preview Request a preview build for the initialized source commit.
1154
1212
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
@@ -1161,8 +1219,8 @@ Options:
1161
1219
 
1162
1220
  Examples:
1163
1221
  tender app init artifact_123 --dir ./widget --dry-run --json
1164
- tender app init artifact_123 --dir ./widget --json
1165
- tender app init artifact_123 --dir ./widget --preview --json
1222
+ tender app init artifact_123 --dir ./data-collection-app --preview --json
1223
+ tender app init artifact_123 --dir ./product-survey-runtime --scaffold shopify-pdp-survey-runtime --json
1166
1224
  tender app init artifact_123 --dir ./widget --force --json`;
1167
1225
  }
1168
1226
  function artifactsContextFetchHelp() {
@@ -1305,10 +1363,10 @@ Examples:
1305
1363
  tender app git credential artifact_123 get --remote-url https://app.tenderprompt.com/git/artifacts/artifact_123.git`;
1306
1364
  }
1307
1365
  function previewRebuildHelp() {
1308
- return `Usage: tender app preview rebuild <app-id> [--commit <sha>] [--base-url <url>] [--token <token>] [--profile <name>] [--stream] [--json]
1366
+ return `Usage: tender app preview rebuild <app-id> [--commit <sha-or-ref>] [--base-url <url>] [--token <token>] [--profile <name>] [--stream] [--json]
1309
1367
 
1310
1368
  Options:
1311
- --commit <sha> Rebuild preview only if the app workspace is at this Git commit.
1369
+ --commit <sha-or-ref> Rebuild preview only if the app workspace is at this Git commit. Git refs like HEAD are resolved locally.
1312
1370
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
1313
1371
  --token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
1314
1372
  --profile <name> Local config profile name. Defaults to the saved default profile.
@@ -1334,10 +1392,10 @@ Examples:
1334
1392
  tender app validate artifact_123 --stream --json`;
1335
1393
  }
1336
1394
  function buildHelp() {
1337
- return `Usage: tender app build <app-id> [--commit <sha>] [--base-url <url>] [--token <token>] [--profile <name>] [--stream] [--json]
1395
+ return `Usage: tender app build <app-id> [--commit <sha-or-ref>] [--base-url <url>] [--token <token>] [--profile <name>] [--stream] [--json]
1338
1396
 
1339
1397
  Options:
1340
- --commit <sha> Build only if the app workspace is at this Git commit.
1398
+ --commit <sha-or-ref> Build only if the app workspace is at this Git commit. Git refs like HEAD are resolved locally.
1341
1399
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
1342
1400
  --token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
1343
1401
  --profile <name> Local config profile name. Defaults to the saved default profile.
@@ -1349,10 +1407,10 @@ Examples:
1349
1407
  tender app build artifact_123 --stream --json`;
1350
1408
  }
1351
1409
  function previewStatusHelp() {
1352
- return `Usage: tender app preview status <app-id> [--commit <sha> | --job <job-id>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
1410
+ return `Usage: tender app preview status <app-id> [--commit <sha-or-ref> | --job <job-id>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
1353
1411
 
1354
1412
  Options:
1355
- --commit <sha> Show the latest preview job for one artifact Git commit.
1413
+ --commit <sha-or-ref> Show the latest preview job for one artifact Git commit. Git refs like HEAD are resolved locally.
1356
1414
  --job <job-id> Show one lifecycle job directly.
1357
1415
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
1358
1416
  --token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
@@ -1364,10 +1422,10 @@ Examples:
1364
1422
  tender app preview status artifact_123 --job job_123 --json`;
1365
1423
  }
1366
1424
  function previewWatchHelp() {
1367
- return `Usage: tender app preview watch <app-id> [--commit <sha> | --job <job-id>] [--base-url <url>] [--token <token>] [--profile <name>] [--stream] [--json]
1425
+ return `Usage: tender app preview watch <app-id> [--commit <sha-or-ref> | --job <job-id>] [--base-url <url>] [--token <token>] [--profile <name>] [--stream] [--json]
1368
1426
 
1369
1427
  Options:
1370
- --commit <sha> Watch the latest preview job for one artifact Git commit.
1428
+ --commit <sha-or-ref> Watch the latest preview job for one artifact Git commit. Git refs like HEAD are resolved locally.
1371
1429
  --job <job-id> Watch one lifecycle job directly.
1372
1430
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
1373
1431
  --token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
@@ -1380,12 +1438,12 @@ Examples:
1380
1438
  tender app preview watch artifact_123 --job job_123 --stream --json`;
1381
1439
  }
1382
1440
  function publishHelp() {
1383
- return `Usage: tender app publish <app-id> (--dry-run | --confirm publish) [--commit <sha>] [--message <text>] [--base-url <url>] [--token <token>] [--profile <name>] [--stream] [--json]
1441
+ return `Usage: tender app publish <app-id> (--dry-run | --confirm publish) [--commit <sha-or-ref>] [--message <text>] [--base-url <url>] [--token <token>] [--profile <name>] [--stream] [--json]
1384
1442
 
1385
1443
  Options:
1386
1444
  --dry-run Show the publish request without calling the API.
1387
1445
  --confirm publish Required to call the production-facing publish API.
1388
- --commit <sha> Publish only if the app workspace is at this Git commit.
1446
+ --commit <sha-or-ref> Publish only if the app workspace is at this Git commit. Git refs like HEAD are resolved locally.
1389
1447
  --message <text> Human audit message returned in CLI output.
1390
1448
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
1391
1449
  --token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
@@ -1494,7 +1552,7 @@ function tenderCliCapabilities() {
1494
1552
  cli: {
1495
1553
  name: CLI_PACKAGE_NAME,
1496
1554
  version: CLI_PACKAGE_VERSION,
1497
- latestRunner: 'npm exec --yes @tenderprompt/cli@latest --',
1555
+ latestRunner: 'npm exec --yes --package @tenderprompt/cli@latest -- tender',
1498
1556
  },
1499
1557
  recommendedSkill: RECOMMENDED_TENDER_PROMPT_SKILL,
1500
1558
  playbooks: {
@@ -1560,7 +1618,7 @@ function tenderCliCapabilities() {
1560
1618
  },
1561
1619
  description: 'Agent-safe Tender App source, lifecycle, analytics, and local scaffold controls.',
1562
1620
  discovery: {
1563
- primary: 'npm exec --yes @tenderprompt/cli@latest -- capabilities --json',
1621
+ primary: 'npm exec --yes --package @tenderprompt/cli@latest -- tender capabilities --json',
1564
1622
  layeredHelp: [
1565
1623
  'tender --version',
1566
1624
  'tender --help',
@@ -1578,6 +1636,7 @@ function tenderCliCapabilities() {
1578
1636
  'tender app outbound-auth --help',
1579
1637
  'tender connectors --help',
1580
1638
  'tender connectors shopify source init --help',
1639
+ 'tender shopify workspace --help',
1581
1640
  ],
1582
1641
  },
1583
1642
  workflows: [
@@ -1586,7 +1645,8 @@ function tenderCliCapabilities() {
1586
1645
  when: 'Before reading or changing remote Tender App records.',
1587
1646
  commands: [
1588
1647
  'tender auth status --json',
1589
- 'tender auth login --device --profile edit-preview --artifact <artifact-id> --ttl 7d --json',
1648
+ 'tender auth login --device --profile edit-preview --artifact <artifact-id> --ttl 7d --save-profile agent --json',
1649
+ 'tender auth status --profile agent --json',
1590
1650
  ],
1591
1651
  },
1592
1652
  {
@@ -1621,12 +1681,46 @@ function tenderCliCapabilities() {
1621
1681
  'Fetch shopify-connector-customer-metadata-widget when the requested job involves customer metafields, metaobjects, or profile writes.',
1622
1682
  ],
1623
1683
  },
1684
+ {
1685
+ name: 'hosted_shopify_project_release',
1686
+ when: 'Inside a hosted Shopify project workspace after editing a runtime app and linked Shopify connector source, or when the merchant asks to deploy/release/publish the Shopify connector.',
1687
+ commands: [
1688
+ 'tender shopify workspace connectors list --json',
1689
+ 'tender shopify workspace link-runtime --connector <connector-id> --json',
1690
+ 'cd /workspace/runtime-project && git push origin HEAD:main',
1691
+ 'cd /workspace/runtime-project && commit=$(git rev-parse HEAD) && tender app publish "$TENDER_RUNTIME_ARTIFACT_ID" --commit "$commit" --confirm publish --stream --json',
1692
+ 'cd /workspace/shopify-connector && git push origin HEAD:main',
1693
+ 'cd /workspace/shopify-connector && tender shopify workspace validate-source --json',
1694
+ 'cd /workspace/shopify-connector && tender shopify workspace deploy-source --json',
1695
+ ],
1696
+ notes: [
1697
+ 'If /workspace/shopify-connector is missing, list connectors, link the selected connector, then continue in /workspace/shopify-connector unless link-runtime explicitly returns reloadRequired:true.',
1698
+ 'For connector selection or switching, use tender shopify workspace connectors list --json and tender shopify workspace link-runtime --connector <connector-id> --json; do not use tender connectors list or ask for a Shopify Client ID.',
1699
+ 'When selecting by merchant-provided Shopify app name, prefer an exact normalized label/appName match before any partial match; do not choose a longer connector name that merely contains the requested name.',
1700
+ 'After link-runtime succeeds, tender shopify workspace validate-source and deploy-source automatically use that selected connector.',
1701
+ 'For a deploy-only connector request, skip npm install, npm run check, tender app doctor, and other runtime-project checks; run cd /workspace/shopify-connector && tender shopify workspace deploy-source --json.',
1702
+ 'For a validate-only connector request, run cd /workspace/shopify-connector && tender shopify workspace validate-source --json.',
1703
+ 'Only run runtime-project checks when the merchant asks to change, validate, preview, or publish the runtime app.',
1704
+ 'Publish the Tender runtime first and use the pinned runtime URL from publish JSON to update the linked connector source before deploying Shopify.',
1705
+ 'Use TENDER_SHOPIFY_CONNECTOR_ID or connector.json for Tender connector commands; shopify.app.toml contains the Shopify client_id, not the Tender connector identity.',
1706
+ 'Do not run tender connectors shopify source validate or tender connectors shopify source deploy from hosted workspaces; those commands require an account-scoped Tender CLI token.',
1707
+ 'Use tender shopify workspace validate-source and deploy-source so the connector stored App Automation Token stays server-side.',
1708
+ 'If Shopify CLI is already authenticated in the hosted workspace, direct shopify app config validate and shopify app deploy are allowed.',
1709
+ 'Never ask for Shopify App Automation Tokens, client secrets, Tender tokens, or provider keys in the terminal.',
1710
+ 'Do not release by pushing Git tags; Tender runtime publish plus tender shopify workspace deploy-source are the release path.',
1711
+ 'If tender shopify workspace deploy-source reports a missing or expired App Automation Token, stop and ask the merchant to refresh the Custom App automation token in Tender.',
1712
+ ],
1713
+ },
1624
1714
  {
1625
1715
  name: 'create_new_app_with_preview',
1626
1716
  when: 'When the user wants a new Tender App that is immediately visible.',
1627
- commands: ['tender app create --name <name> --init --dir <dir> --preview --json'],
1717
+ commands: [
1718
+ 'tender app create --name "Data Collection App" --init --dir ./data-collection-app --preview --json',
1719
+ 'tender app create --name "Product Page Survey Runtime" --init --dir ./product-survey-runtime --scaffold shopify-pdp-survey-runtime --preview --json',
1720
+ ],
1628
1721
  notes: [
1629
- 'create --init uses the API-derived scaffold when the new app has no source.',
1722
+ 'create --init defaults to the server-backed scaffold when the new app has no source.',
1723
+ 'Pass --scaffold shopify-pdp-survey-runtime only for the Shopify product page survey starter.',
1630
1724
  '--preview requests a preview build for the initial source commit after the push.',
1631
1725
  ],
1632
1726
  },
@@ -1721,6 +1815,7 @@ function tenderCliCapabilities() {
1721
1815
  when: 'When inspecting app-local database tables from a scoped token.',
1722
1816
  commands: [
1723
1817
  'tender auth login --device --profile db-read --artifact <artifact-id> --ttl 7d --save-profile db --json',
1818
+ 'tender auth status --profile db --json',
1724
1819
  'tender app db capabilities <artifact-id> --profile db --json',
1725
1820
  'tender app db tables <artifact-id> --env published --profile db --json',
1726
1821
  'tender app db schema <artifact-id> --env published --table <table-name> --profile db --json',
@@ -1789,7 +1884,7 @@ function tenderCliCapabilities() {
1789
1884
  },
1790
1885
  {
1791
1886
  command: 'tender app create --name <name> --init --dir <dir> --preview --json',
1792
- purpose: 'Create a new Tender App, initialize API-derived source in a local checkout, push it, and request a preview build.',
1887
+ purpose: 'Create a new Tender App, initialize the default server-backed source in a local checkout, push it, and request a preview build.',
1793
1888
  requiresAuth: true,
1794
1889
  writes: true,
1795
1890
  dryRun: false,
@@ -2040,6 +2135,45 @@ function normalizeStoredProfile(value) {
2040
2135
  ...normalizeStoredShopifyContext(record),
2041
2136
  };
2042
2137
  }
2138
+ function normalizeStoredPendingDeviceAuthorization(value) {
2139
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
2140
+ return null;
2141
+ }
2142
+ const record = value;
2143
+ if (typeof record.baseUrl !== 'string' ||
2144
+ typeof record.deviceCode !== 'string' ||
2145
+ typeof record.userCode !== 'string' ||
2146
+ typeof record.verificationUri !== 'string' ||
2147
+ typeof record.verificationUriComplete !== 'string' ||
2148
+ typeof record.expiresAt !== 'number' ||
2149
+ typeof record.tokenProfile !== 'string' ||
2150
+ typeof record.ttl !== 'string') {
2151
+ return null;
2152
+ }
2153
+ let tokenProfile;
2154
+ let ttl;
2155
+ try {
2156
+ tokenProfile = parseTokenProfile(record.tokenProfile);
2157
+ ttl = parseTtl(record.ttl);
2158
+ }
2159
+ catch {
2160
+ return null;
2161
+ }
2162
+ const artifactId = typeof record.artifactId === 'string' || record.artifactId === null ? record.artifactId : undefined;
2163
+ return {
2164
+ baseUrl: normalizeBaseUrl(record.baseUrl),
2165
+ deviceCode: record.deviceCode,
2166
+ userCode: record.userCode,
2167
+ verificationUri: record.verificationUri,
2168
+ verificationUriComplete: record.verificationUriComplete,
2169
+ expiresAt: record.expiresAt,
2170
+ interval: typeof record.interval === 'number' ? record.interval : undefined,
2171
+ tokenProfile,
2172
+ artifactId,
2173
+ ttl,
2174
+ ...normalizeStoredShopifyContext(record),
2175
+ };
2176
+ }
2043
2177
  function normalizeStoredShopifyContext(record) {
2044
2178
  const shopifyAppUrl = typeof record.shopifyAppUrl === 'string' ? record.shopifyAppUrl : null;
2045
2179
  if (shopifyAppUrl) {
@@ -2083,9 +2217,21 @@ function normalizeConfig(value) {
2083
2217
  }
2084
2218
  }
2085
2219
  }
2220
+ const pendingDeviceAuthorizations = {};
2221
+ if (record.pendingDeviceAuthorizations &&
2222
+ typeof record.pendingDeviceAuthorizations === 'object' &&
2223
+ !Array.isArray(record.pendingDeviceAuthorizations)) {
2224
+ for (const [name, pendingAuthorization] of Object.entries(record.pendingDeviceAuthorizations)) {
2225
+ const normalized = normalizeStoredPendingDeviceAuthorization(pendingAuthorization);
2226
+ if (normalized) {
2227
+ pendingDeviceAuthorizations[name] = normalized;
2228
+ }
2229
+ }
2230
+ }
2086
2231
  return {
2087
2232
  defaultProfile: typeof record.defaultProfile === 'string' && record.defaultProfile ? record.defaultProfile : undefined,
2088
2233
  profiles: Object.keys(profiles).length > 0 ? profiles : undefined,
2234
+ pendingDeviceAuthorizations: Object.keys(pendingDeviceAuthorizations).length > 0 ? pendingDeviceAuthorizations : undefined,
2089
2235
  };
2090
2236
  }
2091
2237
  async function readConfig(env) {
@@ -2152,6 +2298,30 @@ async function resolveApiCredentials(input) {
2152
2298
  profileName,
2153
2299
  };
2154
2300
  }
2301
+ async function resolveLifecycleApiCredentials(input) {
2302
+ const baseUrl = normalizeBaseUrl(input.baseUrl ?? input.env.TENDER_BASE_URL ?? DEFAULT_BASE_URL);
2303
+ if (!input.flagToken &&
2304
+ !input.profileName &&
2305
+ isHostedWorkspaceInternalApiBaseUrl(baseUrl)) {
2306
+ return {
2307
+ baseUrl,
2308
+ token: null,
2309
+ source: 'hosted-workspace',
2310
+ profileName: null,
2311
+ };
2312
+ }
2313
+ return await resolveApiCredentials(input);
2314
+ }
2315
+ function apiHeadersForCredentials(input) {
2316
+ const headers = {
2317
+ accept: input.accept ?? 'application/json',
2318
+ ...(input.jsonBody ? { 'content-type': 'application/json' } : {}),
2319
+ };
2320
+ if (input.credentials.token) {
2321
+ headers.authorization = `Bearer ${input.credentials.token}`;
2322
+ }
2323
+ return headers;
2324
+ }
2155
2325
  function parseProfileName(value, flag) {
2156
2326
  if (!value || value.startsWith('-')) {
2157
2327
  throw new TenderCliUsageError(`${flag} requires a profile name.`);
@@ -2233,8 +2403,8 @@ function parseTtl(value) {
2233
2403
  return value;
2234
2404
  }
2235
2405
  function parseScaffoldTemplate(value) {
2236
- if (value !== 'server-backed' && value !== 'none') {
2237
- throw new TenderCliUsageError('--scaffold must be one of server-backed or none.');
2406
+ if (value !== 'server-backed' && value !== 'shopify-pdp-survey-runtime' && value !== 'none') {
2407
+ throw new TenderCliUsageError('--scaffold must be one of server-backed, shopify-pdp-survey-runtime, or none.');
2238
2408
  }
2239
2409
  return value;
2240
2410
  }
@@ -2542,7 +2712,7 @@ function parseBuildArgs(args) {
2542
2712
  if (arg === '--commit') {
2543
2713
  const value = args[index + 1];
2544
2714
  if (!value || value.startsWith('-')) {
2545
- throw new TenderCliUsageError('--commit requires a Git commit SHA.');
2715
+ throw new TenderCliUsageError('--commit requires a Git commit SHA or Git ref.');
2546
2716
  }
2547
2717
  commitSha = value;
2548
2718
  index += 1;
@@ -2603,7 +2773,7 @@ function parsePreviewRebuildArgs(args) {
2603
2773
  if (arg === '--commit') {
2604
2774
  const value = args[index + 1];
2605
2775
  if (!value || value.startsWith('-')) {
2606
- throw new TenderCliUsageError('--commit requires a Git commit SHA.');
2776
+ throw new TenderCliUsageError('--commit requires a Git commit SHA or Git ref.');
2607
2777
  }
2608
2778
  commitSha = value;
2609
2779
  index += 1;
@@ -2658,7 +2828,7 @@ function readPreviewLookupOptions(input) {
2658
2828
  if (arg === '--commit') {
2659
2829
  const value = input.args[index + 1];
2660
2830
  if (!value || value.startsWith('-')) {
2661
- throw new TenderCliUsageError('--commit requires a commit SHA.');
2831
+ throw new TenderCliUsageError('--commit requires a Git commit SHA or Git ref.');
2662
2832
  }
2663
2833
  commitSha = value;
2664
2834
  index += 1;
@@ -2807,7 +2977,7 @@ function parsePublishArgs(args) {
2807
2977
  if (arg === '--commit') {
2808
2978
  const value = args[index + 1];
2809
2979
  if (!value || value.startsWith('-')) {
2810
- throw new TenderCliUsageError('--commit requires a commit SHA.');
2980
+ throw new TenderCliUsageError('--commit requires a Git commit SHA or Git ref.');
2811
2981
  }
2812
2982
  commitSha = value;
2813
2983
  index += 1;
@@ -3198,7 +3368,8 @@ function parseArtifactsCreateArgs(args) {
3198
3368
  let remoteName = 'tender';
3199
3369
  let ttl = '7d';
3200
3370
  let force = false;
3201
- let scaffold = 'server-backed';
3371
+ let scaffold = DEFAULT_TENDER_APP_SCAFFOLD_TEMPLATE;
3372
+ let scaffoldSpecified = false;
3202
3373
  let preview = false;
3203
3374
  let baseUrl = null;
3204
3375
  let flagToken = null;
@@ -3224,6 +3395,7 @@ function parseArtifactsCreateArgs(args) {
3224
3395
  }
3225
3396
  if (arg === '--no-scaffold') {
3226
3397
  scaffold = 'none';
3398
+ scaffoldSpecified = true;
3227
3399
  continue;
3228
3400
  }
3229
3401
  if (arg === '--name') {
@@ -3274,6 +3446,7 @@ function parseArtifactsCreateArgs(args) {
3274
3446
  }
3275
3447
  if (arg === '--scaffold') {
3276
3448
  scaffold = parseScaffoldTemplate(args[index + 1]);
3449
+ scaffoldSpecified = true;
3277
3450
  index += 1;
3278
3451
  continue;
3279
3452
  }
@@ -3299,7 +3472,7 @@ function parseArtifactsCreateArgs(args) {
3299
3472
  }
3300
3473
  if (!init) {
3301
3474
  if (preview) {
3302
- throw new TenderCliUsageError('--preview requires --init. Example: tender app create --name "Exit Intent Widget" --init --dir ./widget --preview --json');
3475
+ throw new TenderCliUsageError('--preview requires --init. Example: tender app create --name "Data Collection App" --init --dir ./data-collection-app --preview --json');
3303
3476
  }
3304
3477
  if (force) {
3305
3478
  throw new TenderCliUsageError('--force is only valid with --init.');
@@ -3313,7 +3486,7 @@ function parseArtifactsCreateArgs(args) {
3313
3486
  if (ttl !== '7d') {
3314
3487
  throw new TenderCliUsageError('--ttl is only valid with --init.');
3315
3488
  }
3316
- if (scaffold !== 'server-backed') {
3489
+ if (scaffoldSpecified) {
3317
3490
  throw new TenderCliUsageError('--scaffold is only valid with --init.');
3318
3491
  }
3319
3492
  }
@@ -3684,6 +3857,7 @@ function parseConnectorsShopifySourceInitArgs(args) {
3684
3857
  let clientId = null;
3685
3858
  let sourceName = null;
3686
3859
  let scopes = null;
3860
+ let template = DEFAULT_SHOPIFY_CONNECTOR_SOURCE_TEMPLATE;
3687
3861
  let force = false;
3688
3862
  for (let index = 0; index < parsed.remaining.length; index += 1) {
3689
3863
  const arg = parsed.remaining[index];
@@ -3707,6 +3881,11 @@ function parseConnectorsShopifySourceInitArgs(args) {
3707
3881
  index += 1;
3708
3882
  continue;
3709
3883
  }
3884
+ if (arg === '--template') {
3885
+ template = parseShopifyConnectorSourceTemplate(parsed.remaining[index + 1]);
3886
+ index += 1;
3887
+ continue;
3888
+ }
3710
3889
  if (arg === '--force') {
3711
3890
  force = true;
3712
3891
  continue;
@@ -3726,6 +3905,7 @@ function parseConnectorsShopifySourceInitArgs(args) {
3726
3905
  clientId,
3727
3906
  sourceName,
3728
3907
  scopes,
3908
+ template,
3729
3909
  force,
3730
3910
  ...parsed,
3731
3911
  };
@@ -3839,6 +4019,58 @@ function parseConnectorsShopifySourceDeployArgs(args) {
3839
4019
  }
3840
4020
  return { command: 'connectors shopify source deploy', connectorId, dir, clientId, version, message, sourceControlUrl, allowDeletes, confirm, ...parsed };
3841
4021
  }
4022
+ function parseShopifyWorkspaceSourceArgs(args, command) {
4023
+ if (args.includes('--help') || args.includes('-h')) {
4024
+ return { command: 'help', topic: command };
4025
+ }
4026
+ let json = false;
4027
+ for (const arg of args) {
4028
+ if (arg === '--json') {
4029
+ json = true;
4030
+ continue;
4031
+ }
4032
+ throw new TenderCliUsageError(`Unknown ${command} option: ${arg}`);
4033
+ }
4034
+ return { command, json };
4035
+ }
4036
+ function parseShopifyWorkspaceConnectorsListArgs(args) {
4037
+ if (args.includes('--help') || args.includes('-h')) {
4038
+ return { command: 'help', topic: 'shopify workspace connectors list' };
4039
+ }
4040
+ let json = false;
4041
+ for (const arg of args) {
4042
+ if (arg === '--json') {
4043
+ json = true;
4044
+ continue;
4045
+ }
4046
+ throw new TenderCliUsageError(`Unknown shopify workspace connectors list option: ${arg}`);
4047
+ }
4048
+ return { command: 'shopify workspace connectors list', json };
4049
+ }
4050
+ function parseShopifyWorkspaceLinkRuntimeArgs(args) {
4051
+ if (args.includes('--help') || args.includes('-h')) {
4052
+ return { command: 'help', topic: 'shopify workspace link-runtime' };
4053
+ }
4054
+ let connectorId = null;
4055
+ let json = false;
4056
+ for (let index = 0; index < args.length; index += 1) {
4057
+ const arg = args[index];
4058
+ if (arg === '--json') {
4059
+ json = true;
4060
+ continue;
4061
+ }
4062
+ if (arg === '--connector') {
4063
+ connectorId = parseConnectorId(parseRequiredFlagValue(args[index + 1], '--connector'), 'tender shopify workspace link-runtime --connector conn_123 --json');
4064
+ index += 1;
4065
+ continue;
4066
+ }
4067
+ throw new TenderCliUsageError(`Unknown shopify workspace link-runtime option: ${arg}`);
4068
+ }
4069
+ if (!connectorId) {
4070
+ throw new TenderCliUsageError('--connector is required. Example: tender shopify workspace link-runtime --connector conn_123 --json');
4071
+ }
4072
+ return { command: 'shopify workspace link-runtime', connectorId, json };
4073
+ }
3842
4074
  function parseConnectorsShopifyDeleteArgs(args) {
3843
4075
  if (args.includes('--help') || args.includes('-h')) {
3844
4076
  return { command: 'help', topic: 'connectors shopify delete' };
@@ -3991,7 +4223,7 @@ function parseArtifactsInitArgs(args) {
3991
4223
  let flagToken = null;
3992
4224
  let profileName = null;
3993
4225
  let ttl = '7d';
3994
- let scaffold = 'server-backed';
4226
+ let scaffold = DEFAULT_TENDER_APP_SCAFFOLD_TEMPLATE;
3995
4227
  let preview = false;
3996
4228
  let dryRun = false;
3997
4229
  let force = false;
@@ -4494,6 +4726,7 @@ function parseAuthLoginArgs(args) {
4494
4726
  let ttl = '7d';
4495
4727
  let json = false;
4496
4728
  let noPoll = false;
4729
+ let poll = false;
4497
4730
  let pollTimeoutSeconds = 600;
4498
4731
  for (let index = 0; index < args.length; index += 1) {
4499
4732
  const arg = args[index];
@@ -4506,9 +4739,19 @@ function parseAuthLoginArgs(args) {
4506
4739
  continue;
4507
4740
  }
4508
4741
  if (arg === '--no-poll') {
4742
+ if (poll) {
4743
+ throw new TenderCliUsageError('--no-poll cannot be combined with --poll.');
4744
+ }
4509
4745
  noPoll = true;
4510
4746
  continue;
4511
4747
  }
4748
+ if (arg === '--poll') {
4749
+ if (noPoll) {
4750
+ throw new TenderCliUsageError('--poll cannot be combined with --no-poll.');
4751
+ }
4752
+ poll = true;
4753
+ continue;
4754
+ }
4512
4755
  if (arg === '--base-url') {
4513
4756
  baseUrl = parseBaseUrlFlag(args, index);
4514
4757
  index += 1;
@@ -4555,6 +4798,10 @@ function parseAuthLoginArgs(args) {
4555
4798
  continue;
4556
4799
  }
4557
4800
  if (arg === '--poll-timeout') {
4801
+ if (noPoll) {
4802
+ throw new TenderCliUsageError('--poll-timeout cannot be combined with --no-poll.');
4803
+ }
4804
+ poll = true;
4558
4805
  pollTimeoutSeconds = parsePositiveIntegerFlag(args[index + 1], '--poll-timeout');
4559
4806
  index += 1;
4560
4807
  continue;
@@ -4584,7 +4831,7 @@ function parseAuthLoginArgs(args) {
4584
4831
  artifactId,
4585
4832
  ttl,
4586
4833
  json,
4587
- noPoll,
4834
+ noPoll: noPoll || (json && !poll),
4588
4835
  pollTimeoutSeconds,
4589
4836
  };
4590
4837
  }
@@ -6919,6 +7166,30 @@ function parseTenderArgs(args) {
6919
7166
  }
6920
7167
  throw new TenderCliUsageError(`Unknown connectors command: ${args[1]}`);
6921
7168
  }
7169
+ if (args[0] === 'shopify') {
7170
+ if (args[1] === undefined || args[1] === '--help' || args[1] === '-h') {
7171
+ return { command: 'help', topic: 'shopify' };
7172
+ }
7173
+ if (args[1] === 'workspace') {
7174
+ if (args[2] === undefined || args[2] === '--help' || args[2] === '-h') {
7175
+ return { command: 'help', topic: 'shopify workspace' };
7176
+ }
7177
+ if (args[2] === 'connectors' && args[3] === 'list') {
7178
+ return parseShopifyWorkspaceConnectorsListArgs(args.slice(4));
7179
+ }
7180
+ if (args[2] === 'link-runtime') {
7181
+ return parseShopifyWorkspaceLinkRuntimeArgs(args.slice(3));
7182
+ }
7183
+ if (args[2] === 'validate-source') {
7184
+ return parseShopifyWorkspaceSourceArgs(args.slice(3), 'shopify workspace validate-source');
7185
+ }
7186
+ if (args[2] === 'deploy-source') {
7187
+ return parseShopifyWorkspaceSourceArgs(args.slice(3), 'shopify workspace deploy-source');
7188
+ }
7189
+ throw new TenderCliUsageError(`Unknown shopify workspace command: ${args[2]}`);
7190
+ }
7191
+ throw new TenderCliUsageError(`Unknown shopify command: ${args[1]}`);
7192
+ }
6922
7193
  if (args[0] === 'apps') {
6923
7194
  throw new TenderCliUsageError('Unknown command: apps. Use `tender app ...` for Tender App workflows.');
6924
7195
  }
@@ -7629,7 +7900,7 @@ async function requestPlaybooksApi(input) {
7629
7900
  (typeof payload?.reasonCode === 'string' && payload.reasonCode) ||
7630
7901
  `Playbooks request failed with HTTP ${response.status}.`;
7631
7902
  const authHint = response.status === 401
7632
- ? '\nAuthenticate first: tender auth login --device --profile edit-preview --save-profile agent --ttl 7d --json'
7903
+ ? '\nAuthenticate first: tender auth login --device --profile edit-preview --save-profile agent --ttl 7d --json, approve the URL, then run tender auth status --profile agent --json.'
7633
7904
  : '';
7634
7905
  throw new Error([message, `Reason: ${reasonCode}.`, 'Example: tender playbooks list --profile agent --json.'].join('\n') + authHint);
7635
7906
  }
@@ -7637,13 +7908,16 @@ async function requestPlaybooksApi(input) {
7637
7908
  }
7638
7909
  async function requestConnectorsApi(input) {
7639
7910
  const suffix = input.connectorsPath ? `/${input.connectorsPath.replace(/^\/+/, '')}` : '';
7911
+ const headers = {
7912
+ accept: 'application/json',
7913
+ ...(input.body === undefined ? {} : { 'content-type': 'application/json' }),
7914
+ };
7915
+ if (input.token) {
7916
+ headers.authorization = `Bearer ${input.token}`;
7917
+ }
7640
7918
  const response = await input.fetcher(`${input.baseUrl}/api/account/connectors${suffix}`, {
7641
7919
  method: input.method,
7642
- headers: {
7643
- authorization: `Bearer ${input.token}`,
7644
- accept: 'application/json',
7645
- ...(input.body === undefined ? {} : { 'content-type': 'application/json' }),
7646
- },
7920
+ headers,
7647
7921
  body: input.body === undefined ? undefined : JSON.stringify(input.body),
7648
7922
  });
7649
7923
  const payload = response.status === 204 ? { ok: true } : await parseJsonResponse(response);
@@ -7652,10 +7926,189 @@ async function requestConnectorsApi(input) {
7652
7926
  const message = (typeof payload?.message === 'string' && payload.message) ||
7653
7927
  (typeof payload?.reasonCode === 'string' && payload.reasonCode) ||
7654
7928
  `Connector request failed with HTTP ${response.status}.`;
7655
- throw new TenderCliConnectorsApiError([message, `Reason: ${reasonCode}.`, 'Example: tender connectors list --profile account --json.'].join('\n'), reasonCode, response.status, payload ?? null);
7929
+ const example = input.hostedWorkspaceHint && reasonCode === 'account_scope_required'
7930
+ ? 'Hosted workspace example: tender shopify workspace connectors list --json.'
7931
+ : 'Example: tender connectors list --profile account --json.';
7932
+ throw new TenderCliConnectorsApiError([message, `Reason: ${reasonCode}.`, example].join('\n'), reasonCode, response.status, payload ?? null, example.replace(/^Example:\s*/i, '').replace(/^Hosted workspace example:\s*/i, '').replace(/\.$/, ''));
7656
7933
  }
7657
7934
  return payload;
7658
7935
  }
7936
+ function normalizeHostedWorkspaceBaseUrl(value) {
7937
+ const baseUrl = value?.trim().replace(/\/+$/, '') ?? '';
7938
+ if (!baseUrl) {
7939
+ return null;
7940
+ }
7941
+ try {
7942
+ const url = new URL(baseUrl);
7943
+ return url.protocol === 'https:' || url.protocol === 'http:' ? url.toString().replace(/\/+$/, '') : null;
7944
+ }
7945
+ catch {
7946
+ return null;
7947
+ }
7948
+ }
7949
+ function readHostedWorkspaceArtifactId(value) {
7950
+ const artifactId = value?.trim() ?? '';
7951
+ return /^artifact_[A-Za-z0-9_-]+$/.test(artifactId) ? artifactId : null;
7952
+ }
7953
+ function isHostedWorkspaceInternalApiBaseUrl(baseUrl) {
7954
+ try {
7955
+ return new URL(baseUrl).hostname === 'tender-api.tender.test';
7956
+ }
7957
+ catch {
7958
+ return false;
7959
+ }
7960
+ }
7961
+ function hostedWorkspaceApiHeaders(input) {
7962
+ return {
7963
+ accept: 'application/json',
7964
+ ...(input.jsonBody ? { 'content-type': 'application/json' } : {}),
7965
+ };
7966
+ }
7967
+ function readHostedWorkspaceConnectorId(value) {
7968
+ const connectorId = value?.trim() ?? '';
7969
+ return /^conn_[A-Za-z0-9_-]+$/.test(connectorId) ? connectorId : null;
7970
+ }
7971
+ function errorCodeMatches(error, code) {
7972
+ return Boolean(error &&
7973
+ typeof error === 'object' &&
7974
+ 'code' in error &&
7975
+ error.code === code);
7976
+ }
7977
+ function hostedWorkspaceSelectionFilePath(runtime) {
7978
+ const env = runtime.env ?? process.env;
7979
+ const explicitPath = env.TENDER_WORKSPACE_STATE_FILE?.trim();
7980
+ if (explicitPath) {
7981
+ return path.resolve(explicitPath);
7982
+ }
7983
+ const runtimeProjectDir = env.TENDER_RUNTIME_PROJECT_DIR?.trim();
7984
+ if (runtimeProjectDir && path.isAbsolute(runtimeProjectDir)) {
7985
+ return path.join(path.dirname(runtimeProjectDir), '.tender', 'shopify-workspace.json');
7986
+ }
7987
+ const cwd = process.cwd();
7988
+ if (cwd === '/workspace' || cwd.startsWith('/workspace/')) {
7989
+ return '/workspace/.tender/shopify-workspace.json';
7990
+ }
7991
+ return null;
7992
+ }
7993
+ async function writeHostedWorkspaceConnectorSelection(input, runtime) {
7994
+ const filePath = hostedWorkspaceSelectionFilePath(runtime);
7995
+ if (!filePath) {
7996
+ return;
7997
+ }
7998
+ await mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
7999
+ await writeFile(filePath, `${JSON.stringify({
8000
+ version: 1,
8001
+ runtimeArtifactId: input.runtimeArtifactId,
8002
+ connectorId: input.connectorId,
8003
+ selectedAt: Date.now(),
8004
+ }, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
8005
+ }
8006
+ async function readHostedWorkspaceConnectorSelection(runtimeArtifactId, runtime) {
8007
+ const env = runtime.env ?? process.env;
8008
+ const envConnectorId = readHostedWorkspaceConnectorId(env.TENDER_SHOPIFY_CONNECTOR_ID);
8009
+ if (envConnectorId) {
8010
+ return envConnectorId;
8011
+ }
8012
+ const filePath = hostedWorkspaceSelectionFilePath(runtime);
8013
+ if (!filePath) {
8014
+ return null;
8015
+ }
8016
+ let content;
8017
+ try {
8018
+ content = await readFile(filePath, 'utf8');
8019
+ }
8020
+ catch (error) {
8021
+ if (errorCodeMatches(error, 'ENOENT')) {
8022
+ return null;
8023
+ }
8024
+ throw error;
8025
+ }
8026
+ let parsed;
8027
+ try {
8028
+ parsed = JSON.parse(content);
8029
+ }
8030
+ catch {
8031
+ return null;
8032
+ }
8033
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
8034
+ return null;
8035
+ }
8036
+ const record = parsed;
8037
+ if (record.runtimeArtifactId !== runtimeArtifactId) {
8038
+ return null;
8039
+ }
8040
+ return readHostedWorkspaceConnectorId(typeof record.connectorId === 'string' ? record.connectorId : undefined);
8041
+ }
8042
+ function shopifyWorkspaceCommandApiPath(command) {
8043
+ return command === 'shopify workspace deploy-source' ? 'deploy' : 'validate';
8044
+ }
8045
+ async function requestShopifyWorkspaceSourceCommand(input) {
8046
+ const url = `${input.baseUrl}/api/account/connectors/${encodeURIComponent(input.connectorId)}/source/${shopifyWorkspaceCommandApiPath(input.command)}`;
8047
+ const response = await input.fetcher(url, {
8048
+ method: 'POST',
8049
+ headers: hostedWorkspaceApiHeaders({
8050
+ jsonBody: true,
8051
+ }),
8052
+ body: JSON.stringify({
8053
+ sourceArtifactId: input.sourceArtifactId,
8054
+ runtimeArtifactId: input.runtimeArtifactId,
8055
+ }),
8056
+ });
8057
+ const payload = response.status === 204 ? { ok: true } : await parseJsonResponse(response);
8058
+ if (payload) {
8059
+ return { status: response.status, payload };
8060
+ }
8061
+ return {
8062
+ status: response.status,
8063
+ payload: {
8064
+ ok: false,
8065
+ reasonCode: `http_${response.status}`,
8066
+ message: `Shopify workspace request failed with HTTP ${response.status}.`,
8067
+ },
8068
+ };
8069
+ }
8070
+ async function requestShopifyWorkspaceConnectorsList(input) {
8071
+ const params = new URLSearchParams({ runtimeArtifactId: input.runtimeArtifactId });
8072
+ const url = `${input.baseUrl}/api/account/connectors?${params.toString()}`;
8073
+ const response = await input.fetcher(url, {
8074
+ method: 'GET',
8075
+ headers: hostedWorkspaceApiHeaders({}),
8076
+ });
8077
+ const payload = response.status === 204 ? { ok: true } : await parseJsonResponse(response);
8078
+ if (payload) {
8079
+ return { status: response.status, payload };
8080
+ }
8081
+ return {
8082
+ status: response.status,
8083
+ payload: {
8084
+ ok: false,
8085
+ reasonCode: `http_${response.status}`,
8086
+ message: `Shopify workspace connectors request failed with HTTP ${response.status}.`,
8087
+ },
8088
+ };
8089
+ }
8090
+ async function requestShopifyWorkspaceLinkRuntime(input) {
8091
+ const url = `${input.baseUrl}/api/account/connectors/${encodeURIComponent(input.connectorId)}/runtime-links`;
8092
+ const response = await input.fetcher(url, {
8093
+ method: 'POST',
8094
+ headers: hostedWorkspaceApiHeaders({
8095
+ jsonBody: true,
8096
+ }),
8097
+ body: JSON.stringify({ runtimeArtifactId: input.runtimeArtifactId }),
8098
+ });
8099
+ const payload = response.status === 204 ? { ok: true } : await parseJsonResponse(response);
8100
+ if (payload) {
8101
+ return { status: response.status, payload };
8102
+ }
8103
+ return {
8104
+ status: response.status,
8105
+ payload: {
8106
+ ok: false,
8107
+ reasonCode: `http_${response.status}`,
8108
+ message: `Shopify workspace link request failed with HTTP ${response.status}.`,
8109
+ },
8110
+ };
8111
+ }
7659
8112
  function dbCorrectedExample(artifactId, dbPath) {
7660
8113
  if (dbPath === 'capabilities') {
7661
8114
  return `tender app db capabilities ${artifactId} --json`;
@@ -8262,10 +8715,208 @@ async function planSourceFiles(input) {
8262
8715
  }
8263
8716
  return planned.sort((left, right) => left.path.localeCompare(right.path));
8264
8717
  }
8718
+ function buildAuthStatusNextCommand(profileName) {
8719
+ return `tender auth status --profile ${profileName} --json`;
8720
+ }
8721
+ function buildAuthLoginRetryCommand(profileName, pendingAuthorization) {
8722
+ const parts = [
8723
+ 'tender auth login --device',
8724
+ `--base-url ${pendingAuthorization.baseUrl}`,
8725
+ `--profile ${pendingAuthorization.tokenProfile}`,
8726
+ ];
8727
+ if (pendingAuthorization.artifactId) {
8728
+ parts.push(`--artifact ${pendingAuthorization.artifactId}`);
8729
+ }
8730
+ parts.push(`--ttl ${pendingAuthorization.ttl}`, `--save-profile ${profileName}`);
8731
+ if (pendingAuthorization.shopifyAppUrl) {
8732
+ parts.push(`--shopify-app-url ${pendingAuthorization.shopifyAppUrl}`);
8733
+ }
8734
+ parts.push('--json');
8735
+ return parts.join(' ');
8736
+ }
8737
+ function pendingDeviceAuthorizationOutput(input) {
8738
+ return {
8739
+ ok: false,
8740
+ command: input.command,
8741
+ status: 'authorization_pending',
8742
+ authenticated: false,
8743
+ pending: true,
8744
+ baseUrl: input.baseUrl,
8745
+ profile: input.pendingAuthorization.tokenProfile,
8746
+ saveProfile: input.profileName,
8747
+ pendingProfile: input.profileName,
8748
+ artifactId: input.pendingAuthorization.artifactId ?? null,
8749
+ shopifyAppUrl: input.pendingAuthorization.shopifyAppUrl ?? null,
8750
+ shopifyStore: input.pendingAuthorization.shopifyStore ?? null,
8751
+ userCode: input.pendingAuthorization.userCode,
8752
+ verificationUri: input.pendingAuthorization.verificationUri,
8753
+ verificationUriComplete: input.pendingAuthorization.verificationUriComplete,
8754
+ expiresAt: formatExpiry(input.pendingAuthorization.expiresAt),
8755
+ interval: input.pendingAuthorization.interval ?? null,
8756
+ configPath: input.configPath,
8757
+ nextCommand: buildAuthStatusNextCommand(input.profileName),
8758
+ };
8759
+ }
8760
+ function storedProfileFromOAuthToken(input) {
8761
+ return {
8762
+ baseUrl: input.baseUrl,
8763
+ token: input.tokenPayload.access_token ?? '',
8764
+ tokenPrefix: input.tokenPayload.token_prefix,
8765
+ tenantId: input.tokenPayload.tenant_id,
8766
+ artifactId: input.tokenPayload.artifact_id,
8767
+ expiresAt: input.tokenPayload.expires_at,
8768
+ permissions: splitScope(input.tokenPayload.scope),
8769
+ ...(input.shopifyAppUrl ? { shopifyAppUrl: input.shopifyAppUrl } : {}),
8770
+ ...(input.shopifyStore ? { shopifyStore: input.shopifyStore } : {}),
8771
+ };
8772
+ }
8773
+ function authLoginSuccessOutput(input) {
8774
+ return {
8775
+ ok: true,
8776
+ command: input.command,
8777
+ baseUrl: input.baseUrl,
8778
+ profile: input.tokenProfile,
8779
+ savedProfile: input.saveProfile,
8780
+ tokenPrefix: input.tokenPayload.token_prefix ?? null,
8781
+ tenantId: input.tokenPayload.tenant_id ?? null,
8782
+ artifactId: input.tokenPayload.artifact_id ?? null,
8783
+ permissions: splitScope(input.tokenPayload.scope),
8784
+ expiresAt: formatExpiry(input.tokenPayload.expires_at),
8785
+ shopifyAppUrl: input.shopifyAppUrl,
8786
+ shopifyStore: input.shopifyStore,
8787
+ configPath: input.configPath,
8788
+ };
8789
+ }
8790
+ function configWithoutPendingDeviceAuthorization(config, profileName) {
8791
+ const pendingDeviceAuthorizations = { ...(config.pendingDeviceAuthorizations ?? {}) };
8792
+ delete pendingDeviceAuthorizations[profileName];
8793
+ return {
8794
+ ...config,
8795
+ pendingDeviceAuthorizations: Object.keys(pendingDeviceAuthorizations).length > 0 ? pendingDeviceAuthorizations : undefined,
8796
+ };
8797
+ }
8798
+ async function completePendingDeviceAuthorization(input) {
8799
+ const configPath = readConfigPath(input.env);
8800
+ if (input.pendingAuthorization.expiresAt <= input.now()) {
8801
+ const updatedConfig = configWithoutPendingDeviceAuthorization(input.config, input.profileName);
8802
+ await writeConfig(input.env, updatedConfig);
8803
+ const expiredResult = {
8804
+ ok: false,
8805
+ command: input.command,
8806
+ status: 'authorization_expired',
8807
+ authenticated: false,
8808
+ pending: false,
8809
+ profile: input.profileName,
8810
+ configPath,
8811
+ nextCommand: buildAuthLoginRetryCommand(input.profileName, input.pendingAuthorization),
8812
+ };
8813
+ if (input.json) {
8814
+ input.io.stdout(JSON.stringify(expiredResult, null, 2));
8815
+ }
8816
+ else {
8817
+ input.io.stdout([
8818
+ 'authenticated: false',
8819
+ `profile: ${input.profileName}`,
8820
+ 'pending_device_auth: expired',
8821
+ `next: ${expiredResult.nextCommand}`,
8822
+ ].join('\n'));
8823
+ }
8824
+ return 1;
8825
+ }
8826
+ const tokenResponse = await input.fetcher(`${input.pendingAuthorization.baseUrl}/oauth/token`, {
8827
+ method: 'POST',
8828
+ headers: {
8829
+ 'content-type': 'application/json',
8830
+ },
8831
+ body: JSON.stringify({
8832
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
8833
+ device_code: input.pendingAuthorization.deviceCode,
8834
+ }),
8835
+ });
8836
+ const tokenPayload = await parseJsonResponse(tokenResponse);
8837
+ if (tokenResponse.ok && tokenPayload?.access_token) {
8838
+ const profiles = input.config.profiles ?? {};
8839
+ profiles[input.profileName] = storedProfileFromOAuthToken({
8840
+ baseUrl: input.pendingAuthorization.baseUrl,
8841
+ tokenPayload,
8842
+ shopifyAppUrl: input.pendingAuthorization.shopifyAppUrl ?? null,
8843
+ shopifyStore: input.pendingAuthorization.shopifyStore ?? null,
8844
+ });
8845
+ const updatedConfig = configWithoutPendingDeviceAuthorization({
8846
+ ...input.config,
8847
+ defaultProfile: input.config.defaultProfile ?? input.profileName,
8848
+ profiles,
8849
+ }, input.profileName);
8850
+ await writeConfig(input.env, updatedConfig);
8851
+ const result = authLoginSuccessOutput({
8852
+ command: input.command,
8853
+ baseUrl: input.pendingAuthorization.baseUrl,
8854
+ tokenProfile: input.pendingAuthorization.tokenProfile,
8855
+ saveProfile: input.profileName,
8856
+ tokenPayload,
8857
+ shopifyAppUrl: input.pendingAuthorization.shopifyAppUrl ?? null,
8858
+ shopifyStore: input.pendingAuthorization.shopifyStore ?? null,
8859
+ configPath,
8860
+ });
8861
+ if (input.json) {
8862
+ input.io.stdout(JSON.stringify(result, null, 2));
8863
+ }
8864
+ else {
8865
+ input.io.stdout([
8866
+ 'authenticated: true',
8867
+ `profile: ${input.profileName}`,
8868
+ `tenant_id: ${result.tenantId ?? '(unknown)'}`,
8869
+ `app_id: ${result.artifactId ?? '(account)'}`,
8870
+ `permissions: ${result.permissions.join(' ') || '(unknown)'}`,
8871
+ `expires_at: ${result.expiresAt ?? '(unknown)'}`,
8872
+ ].join('\n'));
8873
+ }
8874
+ return 0;
8875
+ }
8876
+ if (tokenPayload?.error === 'authorization_pending' || tokenPayload?.error === 'slow_down') {
8877
+ const result = pendingDeviceAuthorizationOutput({
8878
+ command: input.command,
8879
+ baseUrl: input.pendingAuthorization.baseUrl,
8880
+ profileName: input.profileName,
8881
+ pendingAuthorization: input.pendingAuthorization,
8882
+ configPath,
8883
+ });
8884
+ if (input.json) {
8885
+ input.io.stdout(JSON.stringify(result, null, 2));
8886
+ }
8887
+ else {
8888
+ input.io.stdout([
8889
+ 'authenticated: false',
8890
+ `profile: ${input.profileName}`,
8891
+ `verification_url: ${input.pendingAuthorization.verificationUriComplete}`,
8892
+ `user_code: ${input.pendingAuthorization.userCode}`,
8893
+ `next: ${result.nextCommand}`,
8894
+ ].join('\n'));
8895
+ }
8896
+ return 1;
8897
+ }
8898
+ throw new Error(tokenPayload?.error_description ?? tokenPayload?.error ?? `Token exchange failed with HTTP ${tokenResponse.status}.`);
8899
+ }
8265
8900
  async function runAuthStatus(command, io, runtime) {
8266
8901
  const env = runtime.env ?? process.env;
8902
+ const fetcher = runtime.fetcher ?? fetch;
8903
+ const now = runtime.now ?? Date.now;
8267
8904
  if (command.profileName) {
8268
8905
  const config = await readConfig(env);
8906
+ const pendingAuthorization = config.pendingDeviceAuthorizations?.[command.profileName] ?? null;
8907
+ if (pendingAuthorization) {
8908
+ return await completePendingDeviceAuthorization({
8909
+ command: command.command,
8910
+ profileName: command.profileName,
8911
+ config,
8912
+ pendingAuthorization,
8913
+ env,
8914
+ fetcher,
8915
+ io,
8916
+ json: command.json,
8917
+ now,
8918
+ });
8919
+ }
8269
8920
  const profile = config.profiles?.[command.profileName] ?? null;
8270
8921
  const result = {
8271
8922
  ok: Boolean(profile),
@@ -8327,6 +8978,20 @@ async function runAuthStatus(command, io, runtime) {
8327
8978
  }
8328
8979
  const config = await readConfig(env);
8329
8980
  const profileName = command.profileName ?? config.defaultProfile ?? DEFAULT_PROFILE_NAME;
8981
+ const pendingAuthorization = config.pendingDeviceAuthorizations?.[profileName] ?? null;
8982
+ if (pendingAuthorization) {
8983
+ return await completePendingDeviceAuthorization({
8984
+ command: command.command,
8985
+ profileName,
8986
+ config,
8987
+ pendingAuthorization,
8988
+ env,
8989
+ fetcher,
8990
+ io,
8991
+ json: command.json,
8992
+ now,
8993
+ });
8994
+ }
8330
8995
  const profile = config.profiles?.[profileName] ?? null;
8331
8996
  const result = {
8332
8997
  ok: Boolean(profile),
@@ -8391,7 +9056,11 @@ async function runAuthLogin(command, io, runtime) {
8391
9056
  }),
8392
9057
  });
8393
9058
  const devicePayload = await parseJsonResponse(deviceResponse);
8394
- if (!deviceResponse.ok || !devicePayload?.device_code || !devicePayload.user_code || !devicePayload.verification_uri_complete) {
9059
+ if (!deviceResponse.ok ||
9060
+ !devicePayload?.device_code ||
9061
+ !devicePayload.user_code ||
9062
+ !devicePayload.verification_uri ||
9063
+ !devicePayload.verification_uri_complete) {
8395
9064
  throw new Error(devicePayload?.error_description ?? devicePayload?.error ?? `Device authorization failed with HTTP ${deviceResponse.status}.`);
8396
9065
  }
8397
9066
  const verificationUriComplete = shopifyAppUrl
@@ -8401,19 +9070,51 @@ async function runAuthLogin(command, io, runtime) {
8401
9070
  })
8402
9071
  : devicePayload.verification_uri_complete;
8403
9072
  const verificationUri = shopifyAppUrl ? shopifyDeviceVerificationUri(shopifyAppUrl) : devicePayload.verification_uri;
9073
+ const expiresAtMs = now() + Math.max(1, devicePayload.expires_in ?? 600) * 1000;
9074
+ const pendingAuthorization = {
9075
+ baseUrl,
9076
+ deviceCode: devicePayload.device_code,
9077
+ userCode: devicePayload.user_code,
9078
+ verificationUri,
9079
+ verificationUriComplete,
9080
+ expiresAt: expiresAtMs,
9081
+ interval: devicePayload.interval,
9082
+ tokenProfile: command.tokenProfile,
9083
+ artifactId: command.artifactId,
9084
+ ttl: command.ttl,
9085
+ ...(shopifyAppUrl ? { shopifyAppUrl } : {}),
9086
+ ...(shopifyStore ? { shopifyStore } : {}),
9087
+ };
9088
+ const pendingDeviceAuthorizations = {
9089
+ ...(config.pendingDeviceAuthorizations ?? {}),
9090
+ [command.saveProfile]: pendingAuthorization,
9091
+ };
9092
+ const configWithPendingAuthorization = {
9093
+ ...config,
9094
+ pendingDeviceAuthorizations: pendingDeviceAuthorizations,
9095
+ };
9096
+ await writeConfig(env, configWithPendingAuthorization);
8404
9097
  const pendingResult = {
8405
- ok: true,
9098
+ ok: false,
8406
9099
  command: command.command,
9100
+ status: 'authorization_pending',
9101
+ authenticated: false,
9102
+ pending: true,
8407
9103
  baseUrl,
8408
9104
  shopifyAppUrl,
8409
9105
  shopifyStore,
8410
9106
  profile: command.tokenProfile,
8411
9107
  saveProfile: command.saveProfile,
9108
+ pendingProfile: command.saveProfile,
9109
+ artifactId: command.artifactId,
8412
9110
  userCode: devicePayload.user_code,
8413
9111
  verificationUri,
8414
9112
  verificationUriComplete,
8415
9113
  expiresIn: devicePayload.expires_in ?? null,
9114
+ expiresAt: formatExpiry(expiresAtMs),
8416
9115
  interval: devicePayload.interval ?? null,
9116
+ configPath: readConfigPath(env),
9117
+ nextCommand: buildAuthStatusNextCommand(command.saveProfile),
8417
9118
  };
8418
9119
  if (command.noPoll) {
8419
9120
  if (command.json) {
@@ -8424,14 +9125,16 @@ async function runAuthLogin(command, io, runtime) {
8424
9125
  }
8425
9126
  return 0;
8426
9127
  }
8427
- if (!command.json) {
9128
+ if (command.json) {
9129
+ io.stderr(JSON.stringify(pendingResult, null, 2));
9130
+ }
9131
+ else {
8428
9132
  io.stdout([
8429
9133
  `Open this URL to approve Tender CLI access: ${verificationUriComplete}`,
8430
9134
  `User code: ${devicePayload.user_code}`,
8431
9135
  ].join('\n'));
8432
9136
  }
8433
9137
  let intervalSeconds = Math.max(1, devicePayload.interval ?? 5);
8434
- const expiresAtMs = now() + Math.max(1, devicePayload.expires_in ?? 600) * 1000;
8435
9138
  let attemptsRemaining = Math.max(1, Math.ceil(command.pollTimeoutSeconds / intervalSeconds));
8436
9139
  while (attemptsRemaining > 0 && now() < expiresAtMs) {
8437
9140
  attemptsRemaining -= 1;
@@ -8447,38 +9150,29 @@ async function runAuthLogin(command, io, runtime) {
8447
9150
  });
8448
9151
  const tokenPayload = await parseJsonResponse(tokenResponse);
8449
9152
  if (tokenResponse.ok && tokenPayload?.access_token) {
8450
- const profiles = config.profiles ?? {};
8451
- profiles[command.saveProfile] = {
9153
+ const profiles = configWithPendingAuthorization.profiles ?? {};
9154
+ profiles[command.saveProfile] = storedProfileFromOAuthToken({
8452
9155
  baseUrl,
8453
- token: tokenPayload.access_token,
8454
- tokenPrefix: tokenPayload.token_prefix,
8455
- tenantId: tokenPayload.tenant_id,
8456
- artifactId: tokenPayload.artifact_id,
8457
- expiresAt: tokenPayload.expires_at,
8458
- permissions: splitScope(tokenPayload.scope),
8459
- ...(shopifyAppUrl ? { shopifyAppUrl } : {}),
8460
- ...(shopifyStore ? { shopifyStore } : {}),
8461
- };
8462
- await writeConfig(env, {
8463
- ...config,
8464
- defaultProfile: config.defaultProfile ?? command.saveProfile,
8465
- profiles,
9156
+ tokenPayload,
9157
+ shopifyAppUrl,
9158
+ shopifyStore,
8466
9159
  });
8467
- const result = {
8468
- ok: true,
9160
+ const updatedConfig = configWithoutPendingDeviceAuthorization({
9161
+ ...configWithPendingAuthorization,
9162
+ defaultProfile: configWithPendingAuthorization.defaultProfile ?? command.saveProfile,
9163
+ profiles,
9164
+ }, command.saveProfile);
9165
+ await writeConfig(env, updatedConfig);
9166
+ const result = authLoginSuccessOutput({
8469
9167
  command: command.command,
8470
9168
  baseUrl,
8471
- profile: command.tokenProfile,
8472
- savedProfile: command.saveProfile,
8473
- tokenPrefix: tokenPayload.token_prefix ?? null,
8474
- tenantId: tokenPayload.tenant_id ?? null,
8475
- artifactId: tokenPayload.artifact_id ?? null,
8476
- permissions: splitScope(tokenPayload.scope),
8477
- expiresAt: formatExpiry(tokenPayload.expires_at),
9169
+ tokenProfile: command.tokenProfile,
9170
+ saveProfile: command.saveProfile,
9171
+ tokenPayload,
8478
9172
  shopifyAppUrl,
8479
9173
  shopifyStore,
8480
9174
  configPath: readConfigPath(env),
8481
- };
9175
+ });
8482
9176
  if (command.json) {
8483
9177
  io.stdout(JSON.stringify(result, null, 2));
8484
9178
  }
@@ -9116,6 +9810,48 @@ async function runGitCommand(input) {
9116
9810
  }
9117
9811
  return result;
9118
9812
  }
9813
+ async function resolveGitCommitArgument(input) {
9814
+ const value = input.value.trim();
9815
+ if (GIT_COMMIT_SHA_PATTERN.test(value)) {
9816
+ return value.toLowerCase();
9817
+ }
9818
+ if (!value || value.length > 256 || /[\0\r\n]/.test(value)) {
9819
+ throw new TenderCliUsageError('--commit must be a 40-character commit SHA or a Git ref resolvable in this checkout.');
9820
+ }
9821
+ const result = await runGitCommand({
9822
+ runtime: input.runtime,
9823
+ cwd: input.cwd,
9824
+ args: ['rev-parse', '--verify', `${value}^{commit}`],
9825
+ allowFailure: true,
9826
+ });
9827
+ const commitSha = result.stdout.trim().split(/\s+/)[0] ?? '';
9828
+ if (GIT_COMMIT_SHA_PATTERN.test(commitSha)) {
9829
+ return commitSha.toLowerCase();
9830
+ }
9831
+ const detail = result.stderr.trim() || result.stdout.trim();
9832
+ throw new TenderCliUsageError([
9833
+ `--commit must be a 40-character commit SHA or a Git ref resolvable in this checkout: ${value}`,
9834
+ detail,
9835
+ ]
9836
+ .filter(Boolean)
9837
+ .join('\n'));
9838
+ }
9839
+ function readCommandCommitSha(command) {
9840
+ const commitSha = 'commitSha' in command ? command.commitSha : null;
9841
+ return typeof commitSha === 'string' && commitSha.trim() ? commitSha : null;
9842
+ }
9843
+ async function resolveCommandCommitSha(command, runtime) {
9844
+ const requestedCommitSha = readCommandCommitSha(command);
9845
+ if (!requestedCommitSha) {
9846
+ return command;
9847
+ }
9848
+ const commitSha = await resolveGitCommitArgument({
9849
+ runtime,
9850
+ cwd: process.cwd(),
9851
+ value: requestedCommitSha,
9852
+ });
9853
+ return commitSha === requestedCommitSha ? command : { ...command, commitSha };
9854
+ }
9119
9855
  function shellQuote(value) {
9120
9856
  return `'${value.replaceAll("'", "'\\''")}'`;
9121
9857
  }
@@ -9976,29 +10712,30 @@ async function runArtifactsInit(command, io, runtime) {
9976
10712
  return 0;
9977
10713
  }
9978
10714
  async function postLifecycle(input) {
9979
- const credentials = await resolveApiCredentials({
9980
- baseUrl: input.command.baseUrl,
9981
- flagToken: input.command.flagToken,
9982
- profileName: input.command.profileName,
10715
+ const command = await resolveCommandCommitSha(input.command, input.runtime);
10716
+ const credentials = await resolveLifecycleApiCredentials({
10717
+ baseUrl: command.baseUrl,
10718
+ flagToken: command.flagToken,
10719
+ profileName: command.profileName,
9983
10720
  env: input.runtime.env ?? process.env,
9984
10721
  });
9985
10722
  const fetcher = input.runtime.fetcher ?? fetch;
9986
- const stream = input.command.stream;
10723
+ const stream = command.stream;
9987
10724
  const url = `${credentials.baseUrl}${input.path}${stream ? '?stream=1' : ''}`;
9988
10725
  const requestBody = {};
9989
- if ('commitSha' in input.command && input.command.commitSha) {
9990
- requestBody.commitSha = input.command.commitSha;
10726
+ if ('commitSha' in command && command.commitSha) {
10727
+ requestBody.commitSha = command.commitSha;
9991
10728
  }
9992
- if ('message' in input.command && input.command.message) {
9993
- requestBody.message = input.command.message;
10729
+ if ('message' in command && command.message) {
10730
+ requestBody.message = command.message;
9994
10731
  }
9995
10732
  const response = await fetcher(url, {
9996
10733
  method: 'POST',
9997
- headers: {
9998
- authorization: `Bearer ${credentials.token}`,
10734
+ headers: apiHeadersForCredentials({
10735
+ credentials,
9999
10736
  accept: stream ? 'text/event-stream' : 'application/json',
10000
- ...(Object.keys(requestBody).length > 0 ? { 'content-type': 'application/json' } : {}),
10001
- },
10737
+ jsonBody: Object.keys(requestBody).length > 0,
10738
+ }),
10002
10739
  body: Object.keys(requestBody).length > 0 ? JSON.stringify(requestBody) : undefined,
10003
10740
  });
10004
10741
  if (stream) {
@@ -10008,19 +10745,19 @@ async function postLifecycle(input) {
10008
10745
  await emitSseLifecycleStream({
10009
10746
  response,
10010
10747
  io: input.io,
10011
- json: input.command.json,
10748
+ json: command.json,
10012
10749
  });
10013
10750
  await appendAutomaticMemory({
10014
10751
  repoPath: process.cwd(),
10015
- artifactId: input.command.artifactId,
10752
+ artifactId: command.artifactId,
10016
10753
  kind: input.operation === 'publish' ? 'change' : 'validation',
10017
- summary: `${input.command.command} streamed`,
10754
+ summary: `${command.command} streamed`,
10018
10755
  body: `Operation: ${input.operation}\nStatus: streamed`,
10019
10756
  tags: [input.operation, 'stream'],
10020
10757
  status: 'resolved',
10021
10758
  related: {
10022
- commands: [input.command.command],
10023
- ...('commitSha' in input.command && input.command.commitSha ? { releaseId: input.command.commitSha } : {}),
10759
+ commands: [command.command],
10760
+ ...('commitSha' in command && command.commitSha ? { releaseId: command.commitSha } : {}),
10024
10761
  },
10025
10762
  runtime: input.runtime,
10026
10763
  });
@@ -10038,12 +10775,12 @@ async function postLifecycle(input) {
10038
10775
  }
10039
10776
  const result = {
10040
10777
  ok: true,
10041
- command: input.command.command,
10778
+ command: command.command,
10042
10779
  baseUrl: credentials.baseUrl,
10043
10780
  authSource: credentials.source,
10044
10781
  profile: credentials.profileName,
10045
- artifactId: input.command.artifactId,
10046
- appId: input.command.artifactId,
10782
+ artifactId: command.artifactId,
10783
+ appId: command.artifactId,
10047
10784
  operation: payload.operation ?? input.operation,
10048
10785
  job: payload.job ?? null,
10049
10786
  result: payload.result ?? null,
@@ -10053,15 +10790,15 @@ async function postLifecycle(input) {
10053
10790
  const releaseGitRef = input.operation === 'publish' ? readReleaseGitRefFromResult(result.result) : null;
10054
10791
  const memory = await appendAutomaticMemory({
10055
10792
  repoPath,
10056
- artifactId: input.command.artifactId,
10793
+ artifactId: command.artifactId,
10057
10794
  kind: input.operation === 'validate' ? 'validation' : input.operation === 'publish' ? 'change' : 'validation',
10058
- summary: `${input.command.command} completed`,
10795
+ summary: `${command.command} completed`,
10059
10796
  body: `Operation: ${result.operation}\nStatus: completed`,
10060
10797
  tags: [input.operation],
10061
10798
  status: 'resolved',
10062
10799
  related: {
10063
10800
  files: changedFiles,
10064
- commands: [input.command.command],
10801
+ commands: [command.command],
10065
10802
  ...(input.operation === 'preview' ? { previewUrl: readPreviewUrlFromJob(result.job) ?? undefined } : {}),
10066
10803
  ...(input.operation === 'publish' ? { publishedUrl: readObjectString(readObject(result.result), 'publishedUrl') ?? undefined } : {}),
10067
10804
  ...(releaseGitRef?.tagName ? { releaseId: releaseGitRef.tagName } : {}),
@@ -10076,12 +10813,12 @@ async function postLifecycle(input) {
10076
10813
  memory,
10077
10814
  ...(memorySync ? { memorySync } : {}),
10078
10815
  };
10079
- if (input.command.json) {
10816
+ if (command.json) {
10080
10817
  input.io.stdout(JSON.stringify(resultWithMemory, null, 2));
10081
10818
  }
10082
10819
  else {
10083
10820
  input.io.stdout([
10084
- `app_id: ${input.command.artifactId}`,
10821
+ `app_id: ${command.artifactId}`,
10085
10822
  `operation: ${result.operation}`,
10086
10823
  'status: completed',
10087
10824
  ...(input.operation === 'publish' ? formatPublishResultLines(result.result) : []),
@@ -10176,35 +10913,36 @@ async function requestPreviewStatus(input) {
10176
10913
  };
10177
10914
  }
10178
10915
  async function runPreviewStatus(command, io, runtime) {
10916
+ const resolvedCommand = await resolveCommandCommitSha(command, runtime);
10179
10917
  const credentials = await resolveApiCredentials({
10180
- baseUrl: command.baseUrl,
10181
- flagToken: command.flagToken,
10182
- profileName: command.profileName,
10918
+ baseUrl: resolvedCommand.baseUrl,
10919
+ flagToken: resolvedCommand.flagToken,
10920
+ profileName: resolvedCommand.profileName,
10183
10921
  env: runtime.env ?? process.env,
10184
10922
  });
10185
10923
  const { job, preview } = await requestPreviewStatus({
10186
- command,
10924
+ command: resolvedCommand,
10187
10925
  credentials,
10188
10926
  fetcher: runtime.fetcher ?? fetch,
10189
10927
  });
10190
10928
  const result = {
10191
10929
  ok: true,
10192
- command: command.command,
10930
+ command: resolvedCommand.command,
10193
10931
  baseUrl: credentials.baseUrl,
10194
10932
  authSource: credentials.source,
10195
10933
  profile: credentials.profileName,
10196
- artifactId: command.artifactId,
10197
- commitSha: preview.commitSha ?? command.commitSha,
10934
+ artifactId: resolvedCommand.artifactId,
10935
+ commitSha: preview.commitSha ?? resolvedCommand.commitSha,
10198
10936
  status: preview.status ?? job.status ?? null,
10199
10937
  previewUrl: preview.previewUrl ?? readPreviewUrlFromJob(job),
10200
10938
  job,
10201
10939
  };
10202
- if (command.json) {
10940
+ if (resolvedCommand.json) {
10203
10941
  io.stdout(JSON.stringify(result, null, 2));
10204
10942
  }
10205
10943
  else {
10206
10944
  io.stdout([
10207
- `app_id: ${command.artifactId}`,
10945
+ `app_id: ${resolvedCommand.artifactId}`,
10208
10946
  `job_id: ${job.jobId}`,
10209
10947
  `status: ${result.status ?? 'unknown'}`,
10210
10948
  ...(result.previewUrl ? [`preview_url: ${result.previewUrl}`] : []),
@@ -10213,19 +10951,20 @@ async function runPreviewStatus(command, io, runtime) {
10213
10951
  return 0;
10214
10952
  }
10215
10953
  async function runPreviewWatch(command, io, runtime) {
10954
+ const resolvedCommand = await resolveCommandCommitSha(command, runtime);
10216
10955
  const credentials = await resolveApiCredentials({
10217
- baseUrl: command.baseUrl,
10218
- flagToken: command.flagToken,
10219
- profileName: command.profileName,
10956
+ baseUrl: resolvedCommand.baseUrl,
10957
+ flagToken: resolvedCommand.flagToken,
10958
+ profileName: resolvedCommand.profileName,
10220
10959
  env: runtime.env ?? process.env,
10221
10960
  });
10222
10961
  const { job } = await requestPreviewStatus({
10223
- command,
10962
+ command: resolvedCommand,
10224
10963
  credentials,
10225
10964
  fetcher: runtime.fetcher ?? fetch,
10226
10965
  });
10227
- const stream = command.stream;
10228
- const response = await (runtime.fetcher ?? fetch)(`${credentials.baseUrl}/api/v1/artifacts/${encodeURIComponent(command.artifactId)}/jobs/${encodeURIComponent(job.jobId)}/events${stream ? '?stream=1' : ''}`, {
10966
+ const stream = resolvedCommand.stream;
10967
+ const response = await (runtime.fetcher ?? fetch)(`${credentials.baseUrl}/api/v1/artifacts/${encodeURIComponent(resolvedCommand.artifactId)}/jobs/${encodeURIComponent(job.jobId)}/events${stream ? '?stream=1' : ''}`, {
10229
10968
  headers: {
10230
10969
  authorization: `Bearer ${credentials.token}`,
10231
10970
  accept: stream ? 'text/event-stream' : 'application/json',
@@ -10238,7 +10977,7 @@ async function runPreviewWatch(command, io, runtime) {
10238
10977
  await emitSseLifecycleStream({
10239
10978
  response,
10240
10979
  io,
10241
- json: command.json,
10980
+ json: resolvedCommand.json,
10242
10981
  });
10243
10982
  return 0;
10244
10983
  }
@@ -10248,15 +10987,15 @@ async function runPreviewWatch(command, io, runtime) {
10248
10987
  }
10249
10988
  const result = {
10250
10989
  ok: true,
10251
- command: command.command,
10990
+ command: resolvedCommand.command,
10252
10991
  baseUrl: credentials.baseUrl,
10253
10992
  authSource: credentials.source,
10254
10993
  profile: credentials.profileName,
10255
- artifactId: command.artifactId,
10994
+ artifactId: resolvedCommand.artifactId,
10256
10995
  job: payload.job ?? job,
10257
10996
  events: payload.events ?? [],
10258
10997
  };
10259
- if (command.json) {
10998
+ if (resolvedCommand.json) {
10260
10999
  io.stdout(JSON.stringify(result, null, 2));
10261
11000
  }
10262
11001
  else {
@@ -10265,38 +11004,39 @@ async function runPreviewWatch(command, io, runtime) {
10265
11004
  return 0;
10266
11005
  }
10267
11006
  async function runPublish(command, io, runtime) {
10268
- if (command.dryRun) {
11007
+ const resolvedCommand = await resolveCommandCommitSha(command, runtime);
11008
+ if (resolvedCommand.dryRun) {
10269
11009
  const result = {
10270
11010
  ok: true,
10271
- command: command.command,
10272
- artifactId: command.artifactId,
10273
- appId: command.artifactId,
10274
- commitSha: command.commitSha,
11011
+ command: resolvedCommand.command,
11012
+ artifactId: resolvedCommand.artifactId,
11013
+ appId: resolvedCommand.artifactId,
11014
+ commitSha: resolvedCommand.commitSha,
10275
11015
  dryRun: true,
10276
11016
  confirmed: false,
10277
- message: command.message,
11017
+ message: resolvedCommand.message,
10278
11018
  wouldRequest: {
10279
11019
  method: 'POST',
10280
- path: `/api/v1/artifacts/${command.artifactId}/publish`,
10281
- stream: command.stream,
11020
+ path: `/api/v1/artifacts/${resolvedCommand.artifactId}/publish`,
11021
+ stream: resolvedCommand.stream,
10282
11022
  body: {
10283
- ...(command.commitSha ? { commitSha: command.commitSha } : {}),
10284
- ...(command.message ? { message: command.message } : {}),
11023
+ ...(resolvedCommand.commitSha ? { commitSha: resolvedCommand.commitSha } : {}),
11024
+ ...(resolvedCommand.message ? { message: resolvedCommand.message } : {}),
10285
11025
  },
10286
11026
  },
10287
11027
  };
10288
- if (command.json) {
11028
+ if (resolvedCommand.json) {
10289
11029
  io.stdout(JSON.stringify(result, null, 2));
10290
11030
  }
10291
11031
  else {
10292
- io.stdout([`app_id: ${command.artifactId}`, 'dry_run: true', 'next: rerun with --confirm publish to publish.'].join('\n'));
11032
+ io.stdout([`app_id: ${resolvedCommand.artifactId}`, 'dry_run: true', 'next: rerun with --confirm publish to publish.'].join('\n'));
10293
11033
  }
10294
11034
  return 0;
10295
11035
  }
10296
11036
  return await postLifecycle({
10297
- command,
11037
+ command: resolvedCommand,
10298
11038
  operation: 'publish',
10299
- path: `/api/v1/artifacts/${encodeURIComponent(command.artifactId)}/publish`,
11039
+ path: `/api/v1/artifacts/${encodeURIComponent(resolvedCommand.artifactId)}/publish`,
10300
11040
  io,
10301
11041
  runtime,
10302
11042
  });
@@ -10966,7 +11706,7 @@ async function resolvePlaybooksCredentials(command, runtime) {
10966
11706
  if (error instanceof TenderCliUsageError) {
10967
11707
  throw new TenderCliUsageError([
10968
11708
  'Playbooks require Tender authentication.',
10969
- 'Run `tender auth login --device --profile edit-preview --save-profile agent --ttl 7d --json`, set TENDER_API_TOKEN, or pass --token <token>.',
11709
+ 'Run `tender auth login --device --profile edit-preview --save-profile agent --ttl 7d --json`, approve the URL, then run `tender auth status --profile agent --json`; or set TENDER_API_TOKEN or pass --token <token>.',
10970
11710
  ].join('\n'));
10971
11711
  }
10972
11712
  throw error;
@@ -11070,7 +11810,7 @@ async function resolveConnectorsCredentials(command, runtime) {
11070
11810
  if (error instanceof TenderCliUsageError) {
11071
11811
  throw new TenderCliUsageError([
11072
11812
  'Connectors require Tender account authentication.',
11073
- 'Run `tender auth login --device --profile create --ttl 7d --save-profile account --json`, set TENDER_API_TOKEN, or pass --token <token>.',
11813
+ 'Run `tender auth login --device --profile create --ttl 7d --save-profile account --json`, approve the URL, then run `tender auth status --profile account --json`; or set TENDER_API_TOKEN or pass --token <token>.',
11074
11814
  ].join('\n'));
11075
11815
  }
11076
11816
  throw error;
@@ -11095,6 +11835,7 @@ async function runConnectorsList(command, io, runtime) {
11095
11835
  baseUrl: credentials.baseUrl,
11096
11836
  token: credentials.token,
11097
11837
  fetcher: runtime.fetcher ?? fetch,
11838
+ hostedWorkspaceHint: Boolean((runtime.env ?? process.env).TENDER_RUNTIME_ARTIFACT_ID),
11098
11839
  });
11099
11840
  const connectors = readConnectorArray(payload);
11100
11841
  printConnectorsPayload(command, io, payload, connectors.length > 0
@@ -11277,6 +12018,13 @@ function uniqueSortedScopes(scopes) {
11277
12018
  function parseScopeCsv(value) {
11278
12019
  return uniqueSortedScopes((value ?? '').split(/[,\s]+/));
11279
12020
  }
12021
+ const DEFAULT_SHOPIFY_CONNECTOR_SOURCE_TEMPLATE = 'shopify-pdp-survey';
12022
+ function parseShopifyConnectorSourceTemplate(value) {
12023
+ if (value === 'minimal' || value === 'shopify-pdp-survey') {
12024
+ return value;
12025
+ }
12026
+ throw new TenderCliUsageError('--template must be one of minimal or shopify-pdp-survey.');
12027
+ }
11280
12028
  function defaultShopifySourceScopes(connectorScopes, override) {
11281
12029
  if (override) {
11282
12030
  return parseScopeCsv(override);
@@ -11284,6 +12032,108 @@ function defaultShopifySourceScopes(connectorScopes, override) {
11284
12032
  const fromConnector = Array.isArray(connectorScopes) ? connectorScopes.filter((scope) => typeof scope === 'string') : [];
11285
12033
  return uniqueSortedScopes(fromConnector);
11286
12034
  }
12035
+ function buildShopifyPdpSurveySourceTemplateFiles() {
12036
+ const placeholderClientUrl = 'https://example.invalid/tender-product-survey/release-placeholder/client.js';
12037
+ const placeholderStylesUrl = 'https://example.invalid/tender-product-survey/release-placeholder/styles.css';
12038
+ const extensionToml = `name = "Tender product survey"
12039
+ type = "theme"
12040
+ `;
12041
+ const blockLiquid = `{% liquid
12042
+ assign selected_version = block.settings.widget_version | default: 'production'
12043
+ case selected_version
12044
+ when 'release-placeholder'
12045
+ assign runtime_src = '${placeholderClientUrl}'
12046
+ assign stylesheet_src = '${placeholderStylesUrl}'
12047
+ else
12048
+ assign runtime_src = '${placeholderClientUrl}'
12049
+ assign stylesheet_src = '${placeholderStylesUrl}'
12050
+ endcase
12051
+ %}
12052
+
12053
+ <div
12054
+ {{ block.shopify_attributes }}
12055
+ data-tender-product-survey-block
12056
+ data-widget-version="{{ selected_version | escape }}"
12057
+ >
12058
+ <link rel="preload" href="{{ stylesheet_src | escape }}" as="style">
12059
+ <link rel="stylesheet" href="{{ stylesheet_src | escape }}">
12060
+ <script type="module" src="{{ runtime_src | escape }}"></script>
12061
+ <tender-product-survey
12062
+ data-question="{{ block.settings.question | escape }}"
12063
+ data-helper="{{ block.settings.helper_text | escape }}"
12064
+ data-options="{{ block.settings.options | escape }}"
12065
+ data-property-name="_tender_survey_source"
12066
+ data-product-id="{{ product.id }}"
12067
+ data-product-handle="{{ product.handle | escape }}"
12068
+ ></tender-product-survey>
12069
+ </div>
12070
+
12071
+ {% schema %}
12072
+ {
12073
+ "name": "Product survey",
12074
+ "target": "section",
12075
+ "settings": [
12076
+ {
12077
+ "type": "select",
12078
+ "id": "widget_version",
12079
+ "label": "Runtime version",
12080
+ "default": "production",
12081
+ "options": [
12082
+ {
12083
+ "value": "production",
12084
+ "label": "Production pinned release"
12085
+ },
12086
+ {
12087
+ "value": "release-placeholder",
12088
+ "label": "Pinned release placeholder"
12089
+ }
12090
+ ]
12091
+ },
12092
+ {
12093
+ "type": "text",
12094
+ "id": "question",
12095
+ "label": "Question",
12096
+ "default": "How did you hear about this product?"
12097
+ },
12098
+ {
12099
+ "type": "text",
12100
+ "id": "helper_text",
12101
+ "label": "Helper text",
12102
+ "default": "Choose one answer and we will save it with your cart."
12103
+ },
12104
+ {
12105
+ "type": "text",
12106
+ "id": "options",
12107
+ "label": "Options",
12108
+ "default": "Instagram|friend|search|other"
12109
+ }
12110
+ ]
12111
+ }
12112
+ {% endschema %}
12113
+ `;
12114
+ const readme = `# Tender Product Survey App Block
12115
+
12116
+ This theme app extension contains a thin product page app block for the Tender
12117
+ product survey runtime.
12118
+
12119
+ ## Runtime Release Pinning
12120
+
12121
+ Replace the placeholder URLs in \`blocks/product_survey.liquid\` after the
12122
+ runtime is published. Use the exact pinned \`client.js\` and \`styles.css\` URLs
12123
+ from the publish JSON. Keep \`production\` plus recent pinned releases in the
12124
+ \`widget_version\` setting so merchants can switch versions in the theme editor.
12125
+
12126
+ Do not point this block to \`latest\`, and do not expose raw runtime URLs as
12127
+ merchant-editable settings.
12128
+ `;
12129
+ return new Map([
12130
+ ['extensions/tender-product-survey/README.md', readme],
12131
+ ['extensions/tender-product-survey/package.json', '{\n "name": "tender-product-survey",\n "private": true,\n "version": "0.0.0"\n}\n'],
12132
+ ['extensions/tender-product-survey/shopify.extension.toml', extensionToml],
12133
+ ['extensions/tender-product-survey/blocks/product_survey.liquid', blockLiquid],
12134
+ ['extensions/tender-product-survey/locales/en.default.json', '{}\n'],
12135
+ ]);
12136
+ }
11287
12137
  function buildShopifySourceFiles(input) {
11288
12138
  const appName = requireShopifySourceString(input.connector.appName, 'Tender Prompt Apps');
11289
12139
  const adminApiVersion = requireShopifySourceString(input.connector.adminApiVersion, '2026-04');
@@ -11298,6 +12148,7 @@ function buildShopifySourceFiles(input) {
11298
12148
  adminApiVersion,
11299
12149
  clientId: input.clientId,
11300
12150
  scopes,
12151
+ sourceTemplate: input.template,
11301
12152
  secrets: {
11302
12153
  clientSecret: 'stored in Tender connector credentials, not in this source tree',
11303
12154
  appAutomationToken: 'stored in Tender connector secret store, not in this source tree',
@@ -11323,6 +12174,24 @@ api_version = ${tomlString(adminApiVersion)}
11323
12174
  [build]
11324
12175
  automatically_update_urls_on_dev = true
11325
12176
  `;
12177
+ const templateFileSummary = input.template === 'shopify-pdp-survey'
12178
+ ? '- `extensions/tender-product-survey/`: A thin product page app block that loads a pinned Tender runtime release.\n'
12179
+ : '';
12180
+ const addCapabilities = input.template === 'shopify-pdp-survey'
12181
+ ? 'This bootstrap includes the first product page survey app block only. Add other app blocks, web pixels, customer metadata definitions, or app-owned GraphQL resources as separate source edits guided by Shopify connector playbooks.'
12182
+ : 'This bootstrap intentionally does not generate app blocks, web pixels, customer metadata definitions, or app-owned GraphQL resources. Add those as source edits using the Shopify connector playbooks so the source tree stays the durable record of what the merchant-owned app actually contains.';
12183
+ const runtimeReleasePinning = input.template === 'shopify-pdp-survey'
12184
+ ? `## Runtime Release Pin
12185
+
12186
+ The product survey block must load a pinned Tender runtime release. After
12187
+ publishing the survey runtime, replace the placeholder URLs in
12188
+ \`extensions/tender-product-survey/blocks/product_survey.liquid\` with the exact
12189
+ pinned URLs returned by the publish JSON. Keep \`production\` plus recent pinned
12190
+ releases in the \`widget_version\` setting so the merchant can switch versions in
12191
+ the theme editor. Do not point the block at a \`latest\` URL.
12192
+
12193
+ `
12194
+ : '';
11326
12195
  const readme = `# ${appName}
11327
12196
 
11328
12197
  This is the merchant-owned Shopify app source bootstrapped by Tender Prompt for
@@ -11333,13 +12202,13 @@ connector \`${input.connectorId}\`.
11333
12202
  - \`shopify.app.toml\`: Shopify app configuration and reviewed Admin API scopes.
11334
12203
  - \`connector.json\`: Non-secret Tender metadata that maps this source tree back
11335
12204
  to the connector.
12205
+ ${templateFileSummary}
11336
12206
 
11337
12207
  ## Add capabilities
11338
12208
 
11339
- This bootstrap intentionally does not generate app blocks, web pixels, customer
11340
- metadata definitions, or app-owned GraphQL resources. Add those as source edits
11341
- using the Shopify connector playbooks so the source tree stays the durable
11342
- record of what the merchant-owned app actually contains.
12209
+ ${addCapabilities}
12210
+
12211
+ ${runtimeReleasePinning}
11343
12212
 
11344
12213
  ## Secrets
11345
12214
 
@@ -11359,15 +12228,32 @@ npx --yes @shopify/cli@latest app config validate --path . --json
11359
12228
  \`\`\`sh
11360
12229
  tender connectors shopify source deploy ${input.connectorId} --dir . --confirm deploy --profile account --json
11361
12230
  \`\`\`
12231
+
12232
+ Inside the hosted Shopify workspace, commit and push source changes, then
12233
+ validate or deploy from the embedded Shopify app's Deploy source action so
12234
+ Tender can use the connector's stored App Automation Token server-side. If
12235
+ Shopify CLI is already authenticated in the hosted workspace, direct
12236
+ \`shopify app config validate\` and \`shopify app deploy\` are allowed. Do not run
12237
+ \`tender connectors shopify source validate\` or
12238
+ \`tender connectors shopify source deploy\` from hosted workspaces; those
12239
+ commands require an account-scoped Tender CLI token. Never ask for Shopify App
12240
+ Automation Tokens, client secrets, Tender tokens, or provider keys in the
12241
+ terminal.
11362
12242
  `;
12243
+ const files = new Map([
12244
+ ['.gitignore', '.shopify/\nnode_modules/\n.env\n.env.*\n'],
12245
+ ['README.md', readme],
12246
+ ['connector.json', `${JSON.stringify(connectorManifest, null, 2)}\n`],
12247
+ ['shopify.app.toml', shopifyAppToml],
12248
+ ]);
12249
+ if (input.template === 'shopify-pdp-survey') {
12250
+ for (const [relativePath, content] of buildShopifyPdpSurveySourceTemplateFiles()) {
12251
+ files.set(relativePath, content);
12252
+ }
12253
+ }
11363
12254
  return {
11364
12255
  manifest: connectorManifest,
11365
- files: new Map([
11366
- ['.gitignore', '.shopify/\nnode_modules/\n.env\n.env.*\n'],
11367
- ['README.md', readme],
11368
- ['connector.json', `${JSON.stringify(connectorManifest, null, 2)}\n`],
11369
- ['shopify.app.toml', shopifyAppToml],
11370
- ]),
12256
+ files,
11371
12257
  };
11372
12258
  }
11373
12259
  async function fileExists(filePath) {
@@ -11760,6 +12646,7 @@ async function runConnectorsShopifySourceInit(command, io, runtime) {
11760
12646
  connector,
11761
12647
  clientId: command.clientId,
11762
12648
  scopes: command.scopes,
12649
+ template: command.template,
11763
12650
  });
11764
12651
  const files = await writeShopifySourceFiles({ dir, files: source.files, force: command.force });
11765
12652
  const gitCommit = await commitAndPushSourceFiles({
@@ -11896,6 +12783,264 @@ async function runConnectorsShopifySourceDeploy(command, io, runtime) {
11896
12783
  printConnectorsPayload(command, io, { ok: true, command: command.command, connectorId: command.connectorId, sourceArtifactId, dir, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr }, [`connector_id: ${command.connectorId}`, `source_artifact_id: ${sourceArtifactId}`, `source_dir: ${dir}`, 'deploy: completed']);
11897
12784
  return 0;
11898
12785
  }
12786
+ function shopifyWorkspaceResultKey(command) {
12787
+ return command === 'shopify workspace deploy-source' ? 'deployment' : 'validation';
12788
+ }
12789
+ function shopifyWorkspaceCommandSucceeded(command, payload) {
12790
+ if (payload.ok !== true) {
12791
+ return false;
12792
+ }
12793
+ const result = payload[shopifyWorkspaceResultKey(command.command)];
12794
+ if (result && typeof result === 'object' && !Array.isArray(result)) {
12795
+ const ok = result.ok;
12796
+ return ok !== false;
12797
+ }
12798
+ return true;
12799
+ }
12800
+ function shopifyWorkspaceTextLines(command, payload) {
12801
+ const connector = payload.connector && typeof payload.connector === 'object' && !Array.isArray(payload.connector)
12802
+ ? payload.connector
12803
+ : {};
12804
+ const source = payload.source && typeof payload.source === 'object' && !Array.isArray(payload.source)
12805
+ ? payload.source
12806
+ : {};
12807
+ const result = payload[shopifyWorkspaceResultKey(command.command)];
12808
+ const resultRecord = result && typeof result === 'object' && !Array.isArray(result)
12809
+ ? result
12810
+ : {};
12811
+ const succeeded = shopifyWorkspaceCommandSucceeded(command, payload);
12812
+ const verb = command.command === 'shopify workspace deploy-source' ? 'deploy' : 'validate';
12813
+ return [
12814
+ `${verb}: ${succeeded ? 'completed' : 'failed'}`,
12815
+ `connector_id: ${String(connector.connectorId ?? '')}`,
12816
+ `source_artifact_id: ${String(source.sourceArtifactId ?? '')}`,
12817
+ ...(payload.reasonCode ? [`reason: ${String(payload.reasonCode)}`] : []),
12818
+ ...(resultRecord.exitCode !== undefined ? [`exit_code: ${String(resultRecord.exitCode)}`] : []),
12819
+ ...(resultRecord.stderr ? [`stderr: ${String(resultRecord.stderr).trim()}`] : []),
12820
+ ].filter((line) => !line.endsWith(': '));
12821
+ }
12822
+ function readShopifyWorkspaceRuntimeIdentity(runtime) {
12823
+ const env = runtime.env ?? process.env;
12824
+ const baseUrl = normalizeHostedWorkspaceBaseUrl(env.TENDER_BASE_URL);
12825
+ if (!baseUrl) {
12826
+ throw new TenderCliUsageError('TENDER_BASE_URL is required. Open or reload the hosted Shopify workspace and run this command inside its terminal.');
12827
+ }
12828
+ if (!isHostedWorkspaceInternalApiBaseUrl(baseUrl)) {
12829
+ throw new TenderCliUsageError('TENDER_BASE_URL must be http://tender-api.tender.test inside a hosted Shopify workspace. Reload the workspace terminal and try again.');
12830
+ }
12831
+ const runtimeArtifactId = readHostedWorkspaceArtifactId(env.TENDER_RUNTIME_ARTIFACT_ID);
12832
+ if (!runtimeArtifactId) {
12833
+ throw new TenderCliUsageError('TENDER_RUNTIME_ARTIFACT_ID is required. This command only runs inside a hosted Shopify project workspace.');
12834
+ }
12835
+ return { baseUrl, runtimeArtifactId };
12836
+ }
12837
+ function readShopifyWorkspaceConnectorRecords(payload) {
12838
+ return Array.isArray(payload.connectors)
12839
+ ? payload.connectors.filter((connector) => Boolean(connector) && typeof connector === 'object' && !Array.isArray(connector))
12840
+ : [];
12841
+ }
12842
+ function readShopifyWorkspaceConnectorSourceArtifactId(connector) {
12843
+ const sourceProject = connector.sourceProject && typeof connector.sourceProject === 'object' && !Array.isArray(connector.sourceProject)
12844
+ ? connector.sourceProject
12845
+ : null;
12846
+ return readHostedWorkspaceArtifactId(typeof sourceProject?.sourceArtifactId === 'string' ? sourceProject.sourceArtifactId : undefined);
12847
+ }
12848
+ function resolveShopifyWorkspaceSourceCommandConnector(input) {
12849
+ const connectors = readShopifyWorkspaceConnectorRecords(input.payload);
12850
+ const connector = input.selectedConnectorId
12851
+ ? connectors.find((candidate) => candidate.connectorId === input.selectedConnectorId) ?? null
12852
+ : (() => {
12853
+ const linked = connectors.filter((candidate) => candidate.linked === true);
12854
+ return linked.length === 1 ? linked[0] ?? null : null;
12855
+ })();
12856
+ if (!connector) {
12857
+ return {
12858
+ ok: false,
12859
+ payload: {
12860
+ ok: false,
12861
+ reasonCode: input.selectedConnectorId
12862
+ ? 'shopify_workspace_selected_connector_not_found'
12863
+ : 'shopify_workspace_connector_selection_required',
12864
+ message: input.selectedConnectorId
12865
+ ? 'The selected Shopify connector is no longer available to this workspace.'
12866
+ : 'Link this runtime to one Shopify connector before validating or deploying source.',
12867
+ },
12868
+ };
12869
+ }
12870
+ const connectorId = typeof connector.connectorId === 'string' ? connector.connectorId : null;
12871
+ const sourceArtifactId = readShopifyWorkspaceConnectorSourceArtifactId(connector);
12872
+ if (!connectorId || !sourceArtifactId) {
12873
+ return {
12874
+ ok: false,
12875
+ payload: {
12876
+ ok: false,
12877
+ reasonCode: 'shopify_source_project_required',
12878
+ message: 'The selected Shopify connector does not have a source project.',
12879
+ },
12880
+ };
12881
+ }
12882
+ return {
12883
+ ok: true,
12884
+ connectorId,
12885
+ sourceArtifactId,
12886
+ };
12887
+ }
12888
+ async function runShopifyWorkspaceSourceCommand(command, io, runtime) {
12889
+ const { baseUrl, runtimeArtifactId } = readShopifyWorkspaceRuntimeIdentity(runtime);
12890
+ const selectedConnectorId = await readHostedWorkspaceConnectorSelection(runtimeArtifactId, runtime);
12891
+ const connectorsResult = await requestShopifyWorkspaceConnectorsList({
12892
+ baseUrl,
12893
+ runtimeArtifactId,
12894
+ fetcher: runtime.fetcher ?? fetch,
12895
+ });
12896
+ if (!shopifyWorkspaceRequestSucceeded(connectorsResult)) {
12897
+ const payload = {
12898
+ ...connectorsResult.payload,
12899
+ command: command.command,
12900
+ runtimeArtifactId,
12901
+ ...(selectedConnectorId ? { selectedConnectorId } : {}),
12902
+ };
12903
+ if (command.json) {
12904
+ io.stdout(JSON.stringify(payload, null, 2));
12905
+ }
12906
+ else {
12907
+ io.stdout(shopifyWorkspaceTextLines(command, payload).join('\n'));
12908
+ }
12909
+ return 1;
12910
+ }
12911
+ const resolvedConnector = resolveShopifyWorkspaceSourceCommandConnector({
12912
+ payload: connectorsResult.payload,
12913
+ selectedConnectorId,
12914
+ });
12915
+ if (!resolvedConnector.ok) {
12916
+ const payload = {
12917
+ ...resolvedConnector.payload,
12918
+ command: command.command,
12919
+ runtimeArtifactId,
12920
+ ...(selectedConnectorId ? { selectedConnectorId } : {}),
12921
+ };
12922
+ if (command.json) {
12923
+ io.stdout(JSON.stringify(payload, null, 2));
12924
+ }
12925
+ else {
12926
+ io.stdout(shopifyWorkspaceTextLines(command, payload).join('\n'));
12927
+ }
12928
+ return 1;
12929
+ }
12930
+ const result = await requestShopifyWorkspaceSourceCommand({
12931
+ command: command.command,
12932
+ baseUrl,
12933
+ runtimeArtifactId,
12934
+ fetcher: runtime.fetcher ?? fetch,
12935
+ connectorId: resolvedConnector.connectorId,
12936
+ sourceArtifactId: resolvedConnector.sourceArtifactId,
12937
+ });
12938
+ const payload = {
12939
+ ...result.payload,
12940
+ command: command.command,
12941
+ runtimeArtifactId,
12942
+ selectedConnectorId: resolvedConnector.connectorId,
12943
+ };
12944
+ if (command.json) {
12945
+ io.stdout(JSON.stringify(payload, null, 2));
12946
+ }
12947
+ else {
12948
+ io.stdout(shopifyWorkspaceTextLines(command, payload).join('\n'));
12949
+ }
12950
+ return shopifyWorkspaceCommandSucceeded(command, payload) ? 0 : 1;
12951
+ }
12952
+ function shopifyWorkspaceRequestSucceeded(result) {
12953
+ return result.status >= 200 && result.status < 300 && result.payload.ok !== false;
12954
+ }
12955
+ function shopifyWorkspaceConnectorListTextLines(payload) {
12956
+ const connectors = readShopifyWorkspaceConnectorRecords(payload);
12957
+ const lines = [
12958
+ `runtime_artifact_id: ${String(payload.runtimeArtifactId ?? '')}`,
12959
+ `shop_domain: ${String(payload.shopDomain ?? '')}`,
12960
+ `connectors: ${connectors.length}`,
12961
+ ];
12962
+ for (const connector of connectors) {
12963
+ lines.push([
12964
+ String(connector.connectorId ?? ''),
12965
+ String(connector.label ?? connector.appName ?? ''),
12966
+ connector.linked ? 'linked' : 'not_linked',
12967
+ connector.ready ? 'ready' : 'needs_setup',
12968
+ ]
12969
+ .filter(Boolean)
12970
+ .join(' | '));
12971
+ }
12972
+ if (typeof payload.reasonCode === 'string') {
12973
+ lines.push(`reason: ${payload.reasonCode}`);
12974
+ }
12975
+ return lines.filter((line) => !line.endsWith(': '));
12976
+ }
12977
+ function shopifyWorkspaceLinkRuntimeTextLines(payload) {
12978
+ const connector = payload.connector && typeof payload.connector === 'object' && !Array.isArray(payload.connector)
12979
+ ? payload.connector
12980
+ : {};
12981
+ const source = payload.source && typeof payload.source === 'object' && !Array.isArray(payload.source)
12982
+ ? payload.source
12983
+ : {};
12984
+ const workspace = payload.workspace && typeof payload.workspace === 'object' && !Array.isArray(payload.workspace)
12985
+ ? payload.workspace
12986
+ : {};
12987
+ return [
12988
+ `link: ${payload.ok === false ? 'failed' : 'created'}`,
12989
+ `runtime_artifact_id: ${String(payload.runtimeArtifactId ?? '')}`,
12990
+ `connector_id: ${String(connector.connectorId ?? '')}`,
12991
+ `source_artifact_id: ${String(source.sourceArtifactId ?? '')}`,
12992
+ workspace.reloadRequired ? 'next: reload the hosted workspace to mount /workspace/shopify-connector' : '',
12993
+ ...(payload.reasonCode ? [`reason: ${String(payload.reasonCode)}`] : []),
12994
+ ].filter(Boolean);
12995
+ }
12996
+ async function runShopifyWorkspaceConnectorsList(command, io, runtime) {
12997
+ const { baseUrl, runtimeArtifactId } = readShopifyWorkspaceRuntimeIdentity(runtime);
12998
+ const result = await requestShopifyWorkspaceConnectorsList({
12999
+ baseUrl,
13000
+ runtimeArtifactId,
13001
+ fetcher: runtime.fetcher ?? fetch,
13002
+ });
13003
+ const payload = {
13004
+ ...result.payload,
13005
+ command: command.command,
13006
+ runtimeArtifactId,
13007
+ };
13008
+ if (command.json) {
13009
+ io.stdout(JSON.stringify(payload, null, 2));
13010
+ }
13011
+ else {
13012
+ io.stdout(shopifyWorkspaceConnectorListTextLines(payload).join('\n'));
13013
+ }
13014
+ return shopifyWorkspaceRequestSucceeded(result) ? 0 : 1;
13015
+ }
13016
+ async function runShopifyWorkspaceLinkRuntime(command, io, runtime) {
13017
+ const { baseUrl, runtimeArtifactId } = readShopifyWorkspaceRuntimeIdentity(runtime);
13018
+ const result = await requestShopifyWorkspaceLinkRuntime({
13019
+ baseUrl,
13020
+ runtimeArtifactId,
13021
+ connectorId: command.connectorId,
13022
+ fetcher: runtime.fetcher ?? fetch,
13023
+ });
13024
+ if (shopifyWorkspaceRequestSucceeded(result)) {
13025
+ await writeHostedWorkspaceConnectorSelection({
13026
+ runtimeArtifactId,
13027
+ connectorId: command.connectorId,
13028
+ }, runtime);
13029
+ }
13030
+ const payload = {
13031
+ ...result.payload,
13032
+ command: command.command,
13033
+ runtimeArtifactId,
13034
+ connectorId: command.connectorId,
13035
+ };
13036
+ if (command.json) {
13037
+ io.stdout(JSON.stringify(payload, null, 2));
13038
+ }
13039
+ else {
13040
+ io.stdout(shopifyWorkspaceLinkRuntimeTextLines(payload).join('\n'));
13041
+ }
13042
+ return shopifyWorkspaceRequestSucceeded(result) ? 0 : 1;
13043
+ }
11899
13044
  async function runConnectorsShopifyDelete(command, io, runtime) {
11900
13045
  if (command.confirm !== 'delete') {
11901
13046
  throw new TenderCliUsageError('--confirm delete is required. Example: tender connectors shopify delete conn_123 --confirm delete --json');
@@ -12804,6 +13949,9 @@ function exampleForError(error, args, command) {
12804
13949
  if (error instanceof TenderCliDbApiError || error instanceof TenderCliAnalyticsApiError) {
12805
13950
  return error.example;
12806
13951
  }
13952
+ if (error instanceof TenderCliConnectorsApiError) {
13953
+ return error.example;
13954
+ }
12807
13955
  const commandName = commandNameForError(args, command);
12808
13956
  const dbPath = dbPathFromCommandName(commandName);
12809
13957
  const artifactId = artifactIdForError(args, command) ?? 'artifact_123';
@@ -12930,6 +14078,14 @@ export async function runTenderCli(args, io = {
12930
14078
  else if (command.topic === 'connectors bind') {
12931
14079
  io.stdout(connectorsBindHelp());
12932
14080
  }
14081
+ else if (command.topic === 'shopify' ||
14082
+ command.topic === 'shopify workspace' ||
14083
+ command.topic === 'shopify workspace connectors list' ||
14084
+ command.topic === 'shopify workspace link-runtime' ||
14085
+ command.topic === 'shopify workspace validate-source' ||
14086
+ command.topic === 'shopify workspace deploy-source') {
14087
+ io.stdout(shopifyWorkspaceHelp());
14088
+ }
12933
14089
  else if (command.topic === 'auth') {
12934
14090
  io.stdout(authHelp());
12935
14091
  }
@@ -13190,6 +14346,15 @@ export async function runTenderCli(args, io = {
13190
14346
  if (command.command === 'connectors shopify source deploy') {
13191
14347
  return await runConnectorsShopifySourceDeploy(command, io, runtime);
13192
14348
  }
14349
+ if (command.command === 'shopify workspace validate-source' || command.command === 'shopify workspace deploy-source') {
14350
+ return await runShopifyWorkspaceSourceCommand(command, io, runtime);
14351
+ }
14352
+ if (command.command === 'shopify workspace connectors list') {
14353
+ return await runShopifyWorkspaceConnectorsList(command, io, runtime);
14354
+ }
14355
+ if (command.command === 'shopify workspace link-runtime') {
14356
+ return await runShopifyWorkspaceLinkRuntime(command, io, runtime);
14357
+ }
13193
14358
  if (command.command === 'connectors shopify delete') {
13194
14359
  return await runConnectorsShopifyDelete(command, io, runtime);
13195
14360
  }
@@ -13362,7 +14527,7 @@ export async function runTenderCli(args, io = {
13362
14527
  }
13363
14528
  catch (error) {
13364
14529
  if (args.includes('--json') &&
13365
- ((args[0] === 'app' && (args[1] === 'db' || (args[1] === 'agent' && args[2] === 'heartbeat'))) || args[0] === 'playbooks' || args[0] === 'connectors')) {
14530
+ ((args[0] === 'app' && (args[1] === 'db' || (args[1] === 'agent' && args[2] === 'heartbeat'))) || args[0] === 'playbooks' || args[0] === 'connectors' || args[0] === 'shopify')) {
13366
14531
  io.stdout(JSON.stringify(cliErrorPayload(error, args, command), null, 2));
13367
14532
  return error instanceof TenderCliUsageError ? 2 : 1;
13368
14533
  }