@tenderprompt/cli 0.1.33 → 0.1.34

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