@tenderprompt/cli 0.1.33 → 0.1.35

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