@tenderprompt/cli 0.1.28 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -60,6 +60,7 @@ tender --version
60
60
  tender capabilities --json
61
61
 
62
62
  tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d
63
+ tender auth login --device --profile create --shopify-store tender-prompt --json
63
64
 
64
65
  tender app init artifact_123 --dir ./widget --preview --json
65
66
 
package/dist/index.d.ts CHANGED
@@ -9,9 +9,11 @@ type TenderCliRuntime = {
9
9
  sleeper?: (milliseconds: number) => Promise<void>;
10
10
  now?: () => number;
11
11
  stdin?: string;
12
- commandRunner?: (command: string, args: string[], options: {
13
- cwd: string;
14
- }) => Promise<TenderCommandResult>;
12
+ commandRunner?: (command: string, args: string[], options: TenderCommandOptions) => Promise<TenderCommandResult>;
13
+ };
14
+ type TenderCommandOptions = {
15
+ cwd: string;
16
+ env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
15
17
  };
16
18
  type TenderCommandResult = {
17
19
  exitCode: number;
package/dist/index.js CHANGED
@@ -95,6 +95,14 @@ class TenderCliUsageError extends Error {
95
95
  this.name = 'TenderCliUsageError';
96
96
  }
97
97
  }
98
+ class TenderCliGuidedUsageError extends TenderCliUsageError {
99
+ nextAction;
100
+ constructor(message, nextAction) {
101
+ super(message);
102
+ this.name = 'TenderCliGuidedUsageError';
103
+ this.nextAction = nextAction;
104
+ }
105
+ }
98
106
  class TenderCliAnalyticsApiError extends Error {
99
107
  reasonCode;
100
108
  scope;
@@ -121,6 +129,18 @@ class TenderCliDbApiError extends Error {
121
129
  this.nextAction = nextAction;
122
130
  }
123
131
  }
132
+ class TenderCliConnectorsApiError extends Error {
133
+ reasonCode;
134
+ status;
135
+ payload;
136
+ constructor(message, reasonCode, status, payload = null) {
137
+ super(message);
138
+ this.name = 'TenderCliConnectorsApiError';
139
+ this.reasonCode = reasonCode;
140
+ this.status = status;
141
+ this.payload = payload;
142
+ }
143
+ }
124
144
  const DEFAULT_PROFILE_NAME = 'default';
125
145
  const DEFAULT_BASE_URL = 'https://app.tenderprompt.com';
126
146
  const ARTIFACT_SOURCE_EXPORT_MAX_FILES = 1000;
@@ -275,18 +295,21 @@ Commands:
275
295
  Examples:
276
296
  tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d
277
297
  tender auth login --device --profile publish --artifact artifact_123 --ttl 7d --save-profile publish
298
+ tender auth login --device --profile create --shopify-store tender-prompt --json
278
299
  tender auth status --json`;
279
300
  }
280
301
  function authLoginHelp() {
281
- return `Usage: tender auth login --device [--base-url <url>] [--profile <edit-preview|publish|create|db-read>] [--artifact <artifact-id>] [--ttl <7d|14d|30d>] [--save-profile <name>] [--no-poll] [--json]
302
+ return `Usage: tender auth login --device [--base-url <url>] [--shopify-store <store>] [--shopify-app-url <url>] [--profile <edit-preview|publish|create|db-read>] [--artifact <artifact-id>] [--ttl <7d|14d|30d>] [--save-profile <name>] [--no-poll] [--json]
282
303
 
283
304
  Options:
284
305
  --device Use the OAuth device-code flow.
285
306
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL or https://app.tenderprompt.com.
307
+ --shopify-store <store> Approve the device code in Shopify Admin for this store handle.
308
+ --shopify-app-url <url> Approve the device code through a Tender Prompt Shopify App Home URL.
286
309
  --profile <value> Token permission profile: edit-preview, publish, create, or db-read. Defaults to edit-preview.
287
310
  --artifact <artifact-id> Scope the token to one artifact. Required for normal edit-preview agent work.
288
311
  --ttl <value> Token lifetime: 7d, 14d, or 30d. Defaults to 7d.
289
- --save-profile <name> Local config profile name. Defaults to default.
312
+ --save-profile <name> Local config profile name. Defaults to default, or shopify-<store> with Shopify approval.
290
313
  --no-poll Print the verification URL and exit without waiting for approval.
291
314
  --json Emit stable JSON for coding agents.
292
315
 
@@ -294,6 +317,7 @@ Examples:
294
317
  tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d
295
318
  tender auth login --device --profile publish --artifact artifact_123 --ttl 7d --save-profile publish --json
296
319
  tender auth login --device --profile create --ttl 7d --save-profile account
320
+ tender auth login --device --profile create --shopify-store tender-prompt --json
297
321
  tender auth login --device --profile db-read --artifact artifact_123 --ttl 7d --save-profile db`;
298
322
  }
299
323
  function authStatusHelp() {
@@ -433,9 +457,9 @@ Examples:
433
457
  printf "$SHOPIFY_CLIENT_SECRET" | tender connectors shopify credentials set conn_123 --shop demo.myshopify.com --client-id abc --client-secret-stdin --profile account --json
434
458
  printf "$SHOPIFY_APP_AUTOMATION_TOKEN" | tender connectors shopify app-automation-token set conn_123 --token-stdin --expires-at 2026-08-22 --profile account --json
435
459
  tender connectors shopify verify conn_123 --profile account --json
436
- tender connectors shopify source init conn_123 --dir ./shopify-app --client-id abc --tender-app-url https://example.tenderprompt.app/client.js --tender-element tender-widget --profile account --json
460
+ tender connectors shopify source init conn_123 --dir ./shopify-app --client-id abc --profile account --json
437
461
  tender connectors shopify source validate conn_123 --dir ./shopify-app --execute --json
438
- SHOPIFY_APP_AUTOMATION_TOKEN=shpat_... tender connectors shopify source deploy conn_123 --dir ./shopify-app --confirm deploy --json
462
+ tender connectors shopify source deploy conn_123 --dir ./shopify-app --confirm deploy --profile account --json
439
463
  tender connectors bind SHOPIFY_CONNECTOR conn_123 --instance default --json`;
440
464
  }
441
465
  function connectorsShopifyCreateHelp() {
@@ -493,16 +517,19 @@ Examples:
493
517
  tender connectors shopify verify conn_123 --profile account --json`;
494
518
  }
495
519
  function connectorsShopifySourceInitHelp() {
496
- return `Usage: tender connectors shopify source init <connector-id> --dir <path> --client-id <shopify-client-id> --tender-app-url <client.js-url> --tender-element <custom-element> [--source-name <name>] [--artifact-id <id>] [--public-id <id>] [--theme-extension-handle <handle>] [--theme-extension-uid <uid>] [--block-name <name>] [--scopes <csv>] [--force] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
520
+ return `Usage: tender connectors shopify source init <connector-id> --dir <path> --client-id <shopify-client-id> [--source-name <name>] [--scopes <csv>] [--force] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
497
521
 
498
522
  Creates or reuses an internal Tender source artifact for the merchant-owned
499
- Shopify app, checks it out to --dir, writes non-secret Shopify app source,
500
- commits it, pushes it to Artifact Git, and records sourceArtifactId on the
501
- connector. The generated source includes shopify.app.toml, dog-profile app-owned
502
- custom data definitions, and a Theme App Extension app-block template.
523
+ Shopify app, checks it out to --dir, writes a minimal non-secret Shopify app
524
+ source tree, commits it, pushes it to Artifact Git, and records
525
+ sourceArtifactId on the connector.
526
+
527
+ The CLI bootstraps and deploys source. App blocks, web pixels, customer
528
+ metadata definitions, and app-owned GraphQL resources are source edits guided by
529
+ playbooks, not source init flags.
503
530
 
504
531
  Examples:
505
- tender connectors shopify source init conn_123 --dir ./shopify-app --client-id abc --tender-app-url https://example.tenderprompt.app/client.js --tender-element tender-widget --profile account --json`;
532
+ tender connectors shopify source init conn_123 --dir ./shopify-app --client-id abc --profile account --json`;
506
533
  }
507
534
  function connectorsShopifySourceCheckoutHelp() {
508
535
  return `Usage: tender connectors shopify source checkout <connector-id> --dir <path> [--remote <name>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
@@ -526,11 +553,13 @@ function connectorsShopifySourceDeployHelp() {
526
553
  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]
527
554
 
528
555
  Without --confirm deploy, prints the exact Shopify CLI deployment command. With
529
- --confirm deploy, runs npx --yes @shopify/cli@latest app deploy. Set
530
- SHOPIFY_APP_AUTOMATION_TOKEN in the environment for non-interactive deploys.
556
+ --confirm deploy, runs npx --yes @shopify/cli@latest app deploy. If
557
+ SHOPIFY_APP_AUTOMATION_TOKEN is not set locally, the CLI requests the
558
+ connector's stored App Automation Token and injects it only into the spawned
559
+ Shopify CLI child process.
531
560
 
532
561
  Examples:
533
- SHOPIFY_APP_AUTOMATION_TOKEN=shpat_... tender connectors shopify source deploy conn_123 --dir ./shopify-app --version dog-profile-smoke --confirm deploy --json`;
562
+ tender connectors shopify source deploy conn_123 --dir ./shopify-app --version connector-source-smoke --confirm deploy --profile account --json`;
534
563
  }
535
564
  function connectorsShopifyDeleteHelp() {
536
565
  return `Usage: tender connectors shopify delete <connector-id> --confirm delete [--base-url <url>] [--token <token>] [--profile <name>] [--json]
@@ -1509,6 +1538,10 @@ function tenderCliCapabilities() {
1509
1538
  targetPackId: 'shopify-connector-source',
1510
1539
  visibility: 'internal_source_artifact_not_customer_facing_app',
1511
1540
  },
1541
+ sourceModel: {
1542
+ cliOwns: ['source artifact lifecycle', 'checkout', 'Shopify CLI validate/deploy forwarding'],
1543
+ playbooksOwn: ['Theme App Extension app blocks', 'web pixels', 'customer metadata definitions', 'Admin GraphQL resource shape'],
1544
+ },
1512
1545
  guardrails: [
1513
1546
  'Use the account profile because connectors are account-level resources.',
1514
1547
  'Use minimum Shopify scopes for the requested job.',
@@ -1584,6 +1617,7 @@ function tenderCliCapabilities() {
1584
1617
  'Connectors are tenant-owned platform bindings, not generated binding workers.',
1585
1618
  'The merchant owns the Shopify custom app; Tender stores connector credentials and app source metadata.',
1586
1619
  'Use connector.sourceProject.sourceArtifactId as the durable Shopify app source identity.',
1620
+ 'Use source init for bootstrap only; app blocks, pixels, metadata definitions, and Admin GraphQL operations are source edits guided by playbooks.',
1587
1621
  'Fetch shopify-connector-customer-metadata-widget when the requested job involves customer metafields, metaobjects, or profile writes.',
1588
1622
  ],
1589
1623
  },
@@ -2101,6 +2135,55 @@ function parseBaseUrlFlag(args, index) {
2101
2135
  }
2102
2136
  return normalizeBaseUrl(value);
2103
2137
  }
2138
+ function parseShopifyStoreHandle(value, flag) {
2139
+ if (!value || value.startsWith('-')) {
2140
+ throw new TenderCliUsageError(`${flag} requires a Shopify Admin store handle.`);
2141
+ }
2142
+ const normalized = value.trim().toLowerCase();
2143
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(normalized)) {
2144
+ throw new TenderCliUsageError(`${flag} must be a Shopify Admin store handle, such as tender-prompt.`);
2145
+ }
2146
+ return normalized;
2147
+ }
2148
+ function shopifyAppUrlForStore(store) {
2149
+ return `https://admin.shopify.com/store/${store}/apps/tender-prompt/shopify`;
2150
+ }
2151
+ function parseShopifyAppUrlFlag(args, index) {
2152
+ const value = args[index + 1];
2153
+ if (!value || value.startsWith('-')) {
2154
+ throw new TenderCliUsageError('--shopify-app-url requires a Shopify Admin App Home URL.');
2155
+ }
2156
+ let url;
2157
+ try {
2158
+ url = new URL(value);
2159
+ }
2160
+ catch {
2161
+ throw new TenderCliUsageError('--shopify-app-url requires a valid Shopify Admin App Home URL.');
2162
+ }
2163
+ if (url.protocol !== 'https:' || url.hostname !== 'admin.shopify.com') {
2164
+ throw new TenderCliUsageError('--shopify-app-url must start with https://admin.shopify.com/.');
2165
+ }
2166
+ const parts = url.pathname.split('/').filter(Boolean);
2167
+ const deviceSuffix = parts[5] === 'device' ? parts[5] : null;
2168
+ if ((parts.length !== 5 && !(parts.length === 6 && deviceSuffix)) ||
2169
+ parts[0] !== 'store' ||
2170
+ parts[2] !== 'apps' ||
2171
+ parts[4] !== 'shopify') {
2172
+ throw new TenderCliUsageError('--shopify-app-url must look like https://admin.shopify.com/store/<store>/apps/tender-prompt/shopify.');
2173
+ }
2174
+ if (url.search || url.hash) {
2175
+ throw new TenderCliUsageError('--shopify-app-url should not include a query string or hash.');
2176
+ }
2177
+ const store = parseShopifyStoreHandle(parts[1], '--shopify-app-url');
2178
+ const appHandle = parseShopifyStoreHandle(parts[3], '--shopify-app-url');
2179
+ return {
2180
+ store,
2181
+ appUrl: `https://admin.shopify.com/store/${store}/apps/${appHandle}/shopify`,
2182
+ };
2183
+ }
2184
+ function shopifyDeviceVerificationUrl(input) {
2185
+ return `${input.shopifyAppUrl.replace(/\/+$/, '')}/device?user_code=${encodeURIComponent(input.userCode)}`;
2186
+ }
2104
2187
  function parseTokenFlag(args, index) {
2105
2188
  const value = args[index + 1];
2106
2189
  if (!value || value.startsWith('-')) {
@@ -3560,18 +3643,11 @@ function parseConnectorsShopifySourceInitArgs(args) {
3560
3643
  if (args.includes('--help') || args.includes('-h')) {
3561
3644
  return { command: 'help', topic: 'connectors shopify source init' };
3562
3645
  }
3563
- const connectorId = parseConnectorId(args[0], 'tender connectors shopify source init <connector-id> --dir ./shopify-app --client-id abc --tender-app-url https://example.tenderprompt.app/client.js --tender-element tender-widget --json');
3646
+ const connectorId = parseConnectorId(args[0], 'tender connectors shopify source init <connector-id> --dir ./shopify-app --client-id abc --json');
3564
3647
  const parsed = parseConnectorsCommonArgs(args, 1);
3565
3648
  let dir = null;
3566
3649
  let clientId = null;
3567
3650
  let sourceName = null;
3568
- let tenderAppUrl = null;
3569
- let tenderElement = null;
3570
- let artifactId = null;
3571
- let publicId = null;
3572
- let themeExtensionHandle = 'tender-widget';
3573
- let themeExtensionUid = null;
3574
- let blockName = 'Tender Prompt Widget';
3575
3651
  let scopes = null;
3576
3652
  let force = false;
3577
3653
  for (let index = 0; index < parsed.remaining.length; index += 1) {
@@ -3591,41 +3667,6 @@ function parseConnectorsShopifySourceInitArgs(args) {
3591
3667
  index += 1;
3592
3668
  continue;
3593
3669
  }
3594
- if (arg === '--tender-app-url') {
3595
- tenderAppUrl = parseRequiredFlagValue(parsed.remaining[index + 1], '--tender-app-url');
3596
- index += 1;
3597
- continue;
3598
- }
3599
- if (arg === '--tender-element') {
3600
- tenderElement = parseRequiredFlagValue(parsed.remaining[index + 1], '--tender-element');
3601
- index += 1;
3602
- continue;
3603
- }
3604
- if (arg === '--artifact-id') {
3605
- artifactId = parseRequiredFlagValue(parsed.remaining[index + 1], '--artifact-id');
3606
- index += 1;
3607
- continue;
3608
- }
3609
- if (arg === '--public-id') {
3610
- publicId = parseRequiredFlagValue(parsed.remaining[index + 1], '--public-id');
3611
- index += 1;
3612
- continue;
3613
- }
3614
- if (arg === '--theme-extension-handle') {
3615
- themeExtensionHandle = parseRequiredFlagValue(parsed.remaining[index + 1], '--theme-extension-handle');
3616
- index += 1;
3617
- continue;
3618
- }
3619
- if (arg === '--theme-extension-uid') {
3620
- themeExtensionUid = parseRequiredFlagValue(parsed.remaining[index + 1], '--theme-extension-uid');
3621
- index += 1;
3622
- continue;
3623
- }
3624
- if (arg === '--block-name') {
3625
- blockName = parseRequiredFlagValue(parsed.remaining[index + 1], '--block-name');
3626
- index += 1;
3627
- continue;
3628
- }
3629
3670
  if (arg === '--scopes') {
3630
3671
  scopes = parseRequiredFlagValue(parsed.remaining[index + 1], '--scopes');
3631
3672
  index += 1;
@@ -3638,31 +3679,17 @@ function parseConnectorsShopifySourceInitArgs(args) {
3638
3679
  throw new TenderCliUsageError(`Unknown connectors shopify source init option: ${arg}`);
3639
3680
  }
3640
3681
  if (!dir) {
3641
- throw new TenderCliUsageError('--dir is required. Example: tender connectors shopify source init conn_123 --dir ./shopify-app --client-id abc --tender-app-url https://example.tenderprompt.app/client.js --tender-element tender-widget --json');
3682
+ throw new TenderCliUsageError('--dir is required. Example: tender connectors shopify source init conn_123 --dir ./shopify-app --client-id abc --json');
3642
3683
  }
3643
3684
  if (!clientId) {
3644
3685
  throw new TenderCliUsageError('--client-id is required because Tender stores only the non-secret client ID prefix.');
3645
3686
  }
3646
- if (!tenderAppUrl) {
3647
- throw new TenderCliUsageError('--tender-app-url is required. Pass the Tender app client.js URL to load in the app block.');
3648
- }
3649
- if (!tenderElement) {
3650
- throw new TenderCliUsageError('--tender-element is required. Pass the Tender custom element tag to mount in Shopify.');
3651
- }
3652
3687
  return {
3653
3688
  command: 'connectors shopify source init',
3654
3689
  connectorId,
3655
3690
  dir,
3656
3691
  clientId,
3657
3692
  sourceName,
3658
- template: 'dog-profile',
3659
- tenderAppUrl,
3660
- tenderElement,
3661
- artifactId,
3662
- publicId,
3663
- themeExtensionHandle,
3664
- themeExtensionUid,
3665
- blockName,
3666
3693
  scopes,
3667
3694
  force,
3668
3695
  ...parsed,
@@ -4423,8 +4450,11 @@ function parseAuthLoginArgs(args) {
4423
4450
  }
4424
4451
  let device = false;
4425
4452
  let baseUrl = null;
4453
+ let shopifyAppUrl = null;
4454
+ let shopifyStore = null;
4426
4455
  let tokenProfile = 'edit-preview';
4427
4456
  let saveProfile = DEFAULT_PROFILE_NAME;
4457
+ let saveProfileExplicit = false;
4428
4458
  let artifactId = null;
4429
4459
  let ttl = '7d';
4430
4460
  let json = false;
@@ -4449,6 +4479,25 @@ function parseAuthLoginArgs(args) {
4449
4479
  index += 1;
4450
4480
  continue;
4451
4481
  }
4482
+ if (arg === '--shopify-store') {
4483
+ if (shopifyAppUrl) {
4484
+ throw new TenderCliUsageError('--shopify-store cannot be combined with --shopify-app-url.');
4485
+ }
4486
+ shopifyStore = parseShopifyStoreHandle(args[index + 1], '--shopify-store');
4487
+ shopifyAppUrl = shopifyAppUrlForStore(shopifyStore);
4488
+ index += 1;
4489
+ continue;
4490
+ }
4491
+ if (arg === '--shopify-app-url') {
4492
+ if (shopifyAppUrl) {
4493
+ throw new TenderCliUsageError('--shopify-app-url cannot be combined with --shopify-store.');
4494
+ }
4495
+ const parsed = parseShopifyAppUrlFlag(args, index);
4496
+ shopifyStore = parsed.store;
4497
+ shopifyAppUrl = parsed.appUrl;
4498
+ index += 1;
4499
+ continue;
4500
+ }
4452
4501
  if (arg === '--profile') {
4453
4502
  tokenProfile = parseTokenProfile(args[index + 1]);
4454
4503
  index += 1;
@@ -4456,6 +4505,7 @@ function parseAuthLoginArgs(args) {
4456
4505
  }
4457
4506
  if (arg === '--save-profile') {
4458
4507
  saveProfile = parseProfileName(args[index + 1], '--save-profile');
4508
+ saveProfileExplicit = true;
4459
4509
  index += 1;
4460
4510
  continue;
4461
4511
  }
@@ -4485,9 +4535,14 @@ function parseAuthLoginArgs(args) {
4485
4535
  if (tokenProfile === 'db-read' && !artifactId) {
4486
4536
  throw new TenderCliUsageError('--profile db-read requires --artifact <artifact-id>.');
4487
4537
  }
4538
+ if (shopifyStore && !saveProfileExplicit) {
4539
+ saveProfile = `shopify-${shopifyStore}`;
4540
+ }
4488
4541
  return {
4489
4542
  command: 'auth login',
4490
4543
  baseUrl,
4544
+ shopifyAppUrl,
4545
+ shopifyStore,
4491
4546
  device,
4492
4547
  tokenProfile,
4493
4548
  saveProfile,
@@ -7562,7 +7617,7 @@ async function requestConnectorsApi(input) {
7562
7617
  const message = (typeof payload?.message === 'string' && payload.message) ||
7563
7618
  (typeof payload?.reasonCode === 'string' && payload.reasonCode) ||
7564
7619
  `Connector request failed with HTTP ${response.status}.`;
7565
- throw new Error([message, `Reason: ${reasonCode}.`, 'Example: tender connectors list --profile account --json.'].join('\n'));
7620
+ throw new TenderCliConnectorsApiError([message, `Reason: ${reasonCode}.`, 'Example: tender connectors list --profile account --json.'].join('\n'), reasonCode, response.status, payload ?? null);
7566
7621
  }
7567
7622
  return payload;
7568
7623
  }
@@ -8275,15 +8330,24 @@ async function runAuthLogin(command, io, runtime) {
8275
8330
  if (!deviceResponse.ok || !devicePayload?.device_code || !devicePayload.user_code || !devicePayload.verification_uri_complete) {
8276
8331
  throw new Error(devicePayload?.error_description ?? devicePayload?.error ?? `Device authorization failed with HTTP ${deviceResponse.status}.`);
8277
8332
  }
8333
+ const verificationUriComplete = command.shopifyAppUrl
8334
+ ? shopifyDeviceVerificationUrl({
8335
+ shopifyAppUrl: command.shopifyAppUrl,
8336
+ userCode: devicePayload.user_code,
8337
+ })
8338
+ : devicePayload.verification_uri_complete;
8339
+ const verificationUri = command.shopifyAppUrl ? `${command.shopifyAppUrl}/device` : devicePayload.verification_uri;
8278
8340
  const pendingResult = {
8279
8341
  ok: true,
8280
8342
  command: command.command,
8281
8343
  baseUrl,
8344
+ shopifyAppUrl: command.shopifyAppUrl,
8345
+ shopifyStore: command.shopifyStore,
8282
8346
  profile: command.tokenProfile,
8283
8347
  saveProfile: command.saveProfile,
8284
8348
  userCode: devicePayload.user_code,
8285
- verificationUri: devicePayload.verification_uri,
8286
- verificationUriComplete: devicePayload.verification_uri_complete,
8349
+ verificationUri,
8350
+ verificationUriComplete,
8287
8351
  expiresIn: devicePayload.expires_in ?? null,
8288
8352
  interval: devicePayload.interval ?? null,
8289
8353
  };
@@ -8292,13 +8356,13 @@ async function runAuthLogin(command, io, runtime) {
8292
8356
  io.stdout(JSON.stringify(pendingResult, null, 2));
8293
8357
  }
8294
8358
  else {
8295
- io.stdout([`verification_url: ${devicePayload.verification_uri_complete}`, `user_code: ${devicePayload.user_code}`].join('\n'));
8359
+ io.stdout([`verification_url: ${verificationUriComplete}`, `user_code: ${devicePayload.user_code}`].join('\n'));
8296
8360
  }
8297
8361
  return 0;
8298
8362
  }
8299
8363
  if (!command.json) {
8300
8364
  io.stdout([
8301
- `Open this URL to approve Tender CLI access: ${devicePayload.verification_uri_complete}`,
8365
+ `Open this URL to approve Tender CLI access: ${verificationUriComplete}`,
8302
8366
  `User code: ${devicePayload.user_code}`,
8303
8367
  ].join('\n'));
8304
8368
  }
@@ -8958,6 +9022,7 @@ function defaultCommandRunner(command, args, options) {
8958
9022
  return new Promise((resolve, reject) => {
8959
9023
  const child = spawn(command, args, {
8960
9024
  cwd: options.cwd,
9025
+ ...(options.env ? { env: options.env } : {}),
8961
9026
  stdio: ['ignore', 'pipe', 'pipe'],
8962
9027
  });
8963
9028
  const stdout = [];
@@ -11136,20 +11201,6 @@ async function runConnectorsShopifyVerify(command, io, runtime) {
11136
11201
  function requireShopifySourceString(value, fallback = '') {
11137
11202
  return typeof value === 'string' && value.trim() ? value.trim() : fallback;
11138
11203
  }
11139
- function normalizeTenderElementTag(value) {
11140
- const normalized = value.trim().toLowerCase();
11141
- if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)+$/.test(normalized)) {
11142
- throw new TenderCliUsageError('--tender-element must be a valid custom element tag, for example tender-dog-profile.');
11143
- }
11144
- return normalized;
11145
- }
11146
- function normalizeThemeExtensionHandle(value) {
11147
- const normalized = value.trim().toLowerCase();
11148
- if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(normalized)) {
11149
- throw new TenderCliUsageError('--theme-extension-handle must contain only lowercase letters, numbers, and dashes.');
11150
- }
11151
- return normalized;
11152
- }
11153
11204
  function tomlString(value) {
11154
11205
  return JSON.stringify(value);
11155
11206
  }
@@ -11159,213 +11210,30 @@ function uniqueSortedScopes(scopes) {
11159
11210
  function parseScopeCsv(value) {
11160
11211
  return uniqueSortedScopes((value ?? '').split(/[,\s]+/));
11161
11212
  }
11162
- function defaultDogProfileScopes(connectorScopes, override) {
11213
+ function defaultShopifySourceScopes(connectorScopes, override) {
11163
11214
  if (override) {
11164
11215
  return parseScopeCsv(override);
11165
11216
  }
11166
11217
  const fromConnector = Array.isArray(connectorScopes) ? connectorScopes.filter((scope) => typeof scope === 'string') : [];
11167
- return uniqueSortedScopes([
11168
- ...fromConnector,
11169
- 'read_customers',
11170
- 'write_customers',
11171
- 'read_metaobject_definitions',
11172
- 'write_metaobject_definitions',
11173
- 'read_metaobjects',
11174
- 'write_metaobjects',
11175
- ]);
11176
- }
11177
- function tenderWidgetAssetJs() {
11178
- return `(function () {
11179
- const loader = (window.TenderPromptBlockLoader ||= {});
11180
- loader.scripts ||= new Map();
11181
- loader.stylesheets ||= new Map();
11182
-
11183
- function loadScript(src) {
11184
- if (!src) return Promise.reject(new Error("Missing Tender runtime URL"));
11185
-
11186
- const existing = loader.scripts.get(src);
11187
- if (existing) return existing.promise;
11188
-
11189
- const promise = new Promise((resolve, reject) => {
11190
- const script = document.createElement("script");
11191
- script.type = "module";
11192
- script.src = src;
11193
- script.onload = resolve;
11194
- script.onerror = () => reject(new Error(\`Failed to load \${src}\`));
11195
- document.head.appendChild(script);
11196
- });
11197
-
11198
- loader.scripts.set(src, { status: "loading", promise });
11199
- promise.then(
11200
- () => loader.scripts.set(src, { status: "loaded", promise }),
11201
- () => loader.scripts.delete(src),
11202
- );
11203
-
11204
- return promise;
11205
- }
11206
-
11207
- function preloadStylesheet(src) {
11208
- if (!src) return Promise.resolve();
11209
- const existing = loader.stylesheets.get(src);
11210
- if (existing) return existing.promise;
11211
-
11212
- const promise = new Promise((resolve) => {
11213
- const link = document.createElement("link");
11214
- link.rel = "preload";
11215
- link.as = "style";
11216
- link.href = src;
11217
- link.onload = resolve;
11218
- link.onerror = resolve;
11219
- document.head.appendChild(link);
11220
- });
11221
-
11222
- loader.stylesheets.set(src, { status: "loading", promise });
11223
- promise.then(() => loader.stylesheets.set(src, { status: "loaded", promise }));
11224
- return promise;
11225
- }
11226
-
11227
- function afterFrame(callback) {
11228
- requestAnimationFrame(() => requestAnimationFrame(callback));
11229
- }
11230
-
11231
- function stylesheetLoaded(href) {
11232
- return performance.getEntriesByName(href).some((entry) => entry.responseEnd > 0);
11233
- }
11234
-
11235
- function waitForWidgetPaintReady(widget, stylesheetUrl) {
11236
- const stylesheetHref = stylesheetUrl ? new URL(stylesheetUrl, document.baseURI).href : null;
11237
-
11238
- return new Promise((resolve) => {
11239
- let settled = false;
11240
- let noShadowTimer = null;
11241
- let rootWithoutStylesheetTimer = null;
11242
- const finish = () => {
11243
- if (settled) return;
11244
- settled = true;
11245
- clearInterval(interval);
11246
- clearTimeout(timeout);
11247
- clearTimeout(noShadowTimer);
11248
- clearTimeout(rootWithoutStylesheetTimer);
11249
- afterFrame(resolve);
11250
- };
11251
-
11252
- const check = () => {
11253
- const root = widget.shadowRoot;
11254
- if (!root) {
11255
- noShadowTimer ||= setTimeout(finish, 350);
11256
- return;
11257
- }
11258
-
11259
- clearTimeout(noShadowTimer);
11260
- const links = Array.from(root.querySelectorAll('link[rel="stylesheet"]'));
11261
- const stylesheet = stylesheetHref
11262
- ? links.find((link) => link.href === stylesheetHref)
11263
- : links[0];
11264
-
11265
- if (!stylesheet) {
11266
- rootWithoutStylesheetTimer ||= setTimeout(finish, 500);
11267
- return;
11268
- }
11269
-
11270
- clearTimeout(rootWithoutStylesheetTimer);
11271
- if (stylesheet.sheet || stylesheetLoaded(stylesheet.href)) {
11272
- finish();
11273
- return;
11274
- }
11275
-
11276
- stylesheet.addEventListener("load", finish, { once: true });
11277
- stylesheet.addEventListener("error", finish, { once: true });
11278
- };
11279
-
11280
- const interval = setInterval(check, 25);
11281
- const timeout = setTimeout(finish, 2200);
11282
- check();
11283
- });
11284
- }
11285
-
11286
- function revealWidget(mount, widget) {
11287
- const placeholder = mount.querySelector("[data-tender-placeholder]");
11288
- widget.style.visibility = "";
11289
- widget.style.position = "";
11290
- widget.style.inset = "";
11291
- widget.style.pointerEvents = "";
11292
- placeholder?.remove();
11293
- mount.dataset.tenderReady = "true";
11294
- mount.setAttribute("aria-busy", "false");
11295
- }
11296
-
11297
- class TenderPromptWidgetBlock extends HTMLElement {
11298
- connectedCallback() {
11299
- this.mount();
11300
- }
11301
-
11302
- async mount() {
11303
- const mount = this.querySelector("[data-tender-mount]");
11304
- const elementTag = this.dataset.tenderElementTag;
11305
- if (!mount || !elementTag || mount.dataset.tenderMounted === "true") return;
11306
-
11307
- mount.setAttribute("aria-busy", "true");
11308
- preloadStylesheet(this.dataset.stylesheetUrl);
11309
- await loadScript(this.dataset.tenderRuntimeSrc);
11310
- await Promise.race([
11311
- customElements.whenDefined(elementTag),
11312
- new Promise((resolve) => setTimeout(resolve, 1000)),
11313
- ]);
11314
-
11315
- const widget = document.createElement(elementTag);
11316
- for (const [key, value] of Object.entries(this.dataset)) {
11317
- if (value == null || key === "tenderRuntimeSrc" || key === "tenderElementTag") continue;
11318
- widget.dataset[key] = value;
11319
- }
11320
-
11321
- widget.style.visibility = "hidden";
11322
- widget.style.position = "absolute";
11323
- widget.style.inset = "0";
11324
- widget.style.pointerEvents = "none";
11325
- mount.appendChild(widget);
11326
- mount.dataset.tenderMounted = "true";
11327
- await waitForWidgetPaintReady(widget, this.dataset.stylesheetUrl);
11328
- revealWidget(mount, widget);
11329
- }
11330
- }
11331
-
11332
- if (!customElements.get("tender-prompt-widget-block")) {
11333
- customElements.define("tender-prompt-widget-block", TenderPromptWidgetBlock);
11334
- }
11335
- })();
11336
- `;
11218
+ return uniqueSortedScopes(fromConnector);
11337
11219
  }
11338
11220
  function buildShopifySourceFiles(input) {
11339
11221
  const appName = requireShopifySourceString(input.connector.appName, 'Tender Prompt Apps');
11340
11222
  const adminApiVersion = requireShopifySourceString(input.connector.adminApiVersion, '2026-04');
11341
11223
  const shopDomain = requireShopifySourceString(input.connector.shopDomain);
11342
- const tenderElement = normalizeTenderElementTag(input.tenderElement);
11343
- const extensionHandle = normalizeThemeExtensionHandle(input.themeExtensionHandle);
11344
- const scopes = defaultDogProfileScopes(input.connector.scopes, input.scopes);
11345
- const artifactId = input.artifactId ?? 'artifact_replace_me';
11346
- const publicId = input.publicId ?? 'tender-widget';
11347
- const stylesheetUrl = new URL('./styles.css', input.tenderAppUrl).href;
11348
- const blockName = input.blockName.trim().slice(0, 120) || 'Tender Prompt Widget';
11224
+ const scopes = defaultShopifySourceScopes(input.connector.scopes, input.scopes);
11349
11225
  const connectorManifest = {
11350
11226
  version: 1,
11351
11227
  kind: 'shopify_connector_source',
11352
11228
  connectorId: input.connectorId,
11353
11229
  instanceId: requireShopifySourceString(input.connector.instanceId, 'default'),
11354
- template: input.template,
11355
11230
  shopDomain: shopDomain || null,
11356
11231
  adminApiVersion,
11357
11232
  clientId: input.clientId,
11358
- themeExtensionHandle: extensionHandle,
11359
- tender: {
11360
- artifactId,
11361
- publicId,
11362
- elementTag: tenderElement,
11363
- runtimeUrl: input.tenderAppUrl,
11364
- stylesheetUrl,
11365
- },
11233
+ scopes,
11366
11234
  secrets: {
11367
11235
  clientSecret: 'stored in Tender connector credentials, not in this source tree',
11368
- appAutomationToken: 'store in SHOPIFY_APP_AUTOMATION_TOKEN or Tender connector secret store, not in git',
11236
+ appAutomationToken: 'stored in Tender connector secret store, not in this source tree',
11369
11237
  },
11370
11238
  };
11371
11239
  const shopifyAppToml = `# Merchant-owned Shopify app source generated by Tender Prompt.
@@ -11387,170 +11255,31 @@ api_version = ${tomlString(adminApiVersion)}
11387
11255
 
11388
11256
  [build]
11389
11257
  automatically_update_urls_on_dev = true
11390
-
11391
- [metaobjects.app.dog_profile]
11392
- name = "Dog profile"
11393
- display_name_field = "dog_name"
11394
- access.admin = "merchant_read_write"
11395
-
11396
- [metaobjects.app.dog_profile.fields.dog_name]
11397
- name = "Dog name"
11398
- type = "single_line_text_field"
11399
- required = true
11400
-
11401
- [metaobjects.app.dog_profile.fields.preferences]
11402
- name = "Preferences"
11403
- type = "json"
11404
-
11405
- [customer.metafields.app.dog_profile]
11406
- name = "Dog profile"
11407
- type = "metaobject_reference<$app:dog_profile>"
11408
- access.admin = "merchant_read_write"
11409
- `;
11410
- const extensionToml = [
11411
- `name = ${tomlString(extensionHandle)}`,
11412
- 'type = "theme"',
11413
- input.themeExtensionUid ? `uid = ${tomlString(input.themeExtensionUid)}` : null,
11414
- '',
11415
- ]
11416
- .filter((line) => line !== null)
11417
- .join('\n');
11418
- const liquid = `{% doc %}
11419
- Tender Prompt app block generated from connector ${input.connectorId}.
11420
-
11421
- The block loads one Tender runtime script and mounts the configured Tender
11422
- custom element. Keep merchant-editable copy and version pins in block settings.
11423
- {% enddoc %}
11424
-
11425
- {% liquid
11426
- assign artifact_id = block.settings.artifact_id | default: ${tomlString(artifactId)}
11427
- assign widget_version = block.settings.widget_version | default: 'latest'
11428
-
11429
- case widget_version
11430
- else
11431
- assign runtime_src = ${tomlString(input.tenderAppUrl)}
11432
- assign stylesheet_src = ${tomlString(stylesheetUrl)}
11433
- endcase
11434
- %}
11435
-
11436
- <tender-prompt-widget-block
11437
- {{ block.shopify_attributes }}
11438
- class="tender-prompt-widget-block tender-prompt-widget-block--{{ block.settings.tone }}"
11439
- data-tender-artifact-id="{{ artifact_id | escape }}"
11440
- data-tender-public-id="{{ block.settings.public_id | escape }}"
11441
- data-tender-version="{{ widget_version | escape }}"
11442
- data-tender-runtime-src="{{ runtime_src | escape }}"
11443
- data-stylesheet-url="{{ stylesheet_src | escape }}"
11444
- data-tender-element-tag="{{ block.settings.element_tag | escape }}"
11445
- data-tender-mode="shopify_app_block"
11446
- data-surface="shopify_theme_app_extension"
11447
- data-tender-block-id="{{ block.id | escape }}"
11448
- data-tender-shop-domain="{{ shop.permanent_domain | escape }}"
11449
- data-tender-heading="{{ block.settings.heading | escape }}"
11450
- data-tender-intro="{{ block.settings.intro | escape }}"
11451
- data-tender-tone="{{ block.settings.tone | escape }}"
11452
- {% if customer %}
11453
- data-tender-customer-id="{{ customer.id }}"
11454
- {% endif %}
11455
- {% if product %}
11456
- data-tender-product-id="{{ product.id }}"
11457
- data-tender-product-handle="{{ product.handle | escape }}"
11458
- {% endif %}
11459
- >
11460
- <div data-tender-mount style="position: relative;">
11461
- <div
11462
- data-tender-placeholder
11463
- style="box-sizing: border-box; display: grid; gap: 14px; min-height: 260px; padding: 28px; border: 1px solid rgba(0,0,0,.12); border-radius: 8px; background: rgba(255,255,255,.78); color: currentColor;"
11464
- >
11465
- <div style="display: grid; gap: 8px;">
11466
- <p style="margin: 0; font: inherit; font-weight: 700;">{{ block.settings.heading }}</p>
11467
- <p style="margin: 0; max-width: 42rem; opacity: .72;">{{ block.settings.intro }}</p>
11468
- </div>
11469
- <div style="display: grid; gap: 10px;" aria-hidden="true">
11470
- <span style="display: block; width: 52%; height: 12px; border-radius: 999px; background: rgba(0,0,0,.10);"></span>
11471
- <span style="display: block; width: 100%; height: 44px; border-radius: 8px; background: rgba(0,0,0,.07);"></span>
11472
- <span style="display: block; width: 88%; height: 44px; border-radius: 8px; background: rgba(0,0,0,.07);"></span>
11473
- </div>
11474
- </div>
11475
- </div>
11476
- </tender-prompt-widget-block>
11477
-
11478
- {% schema %}
11479
- {
11480
- "name": ${JSON.stringify(blockName)},
11481
- "target": "section",
11482
- "javascript": "tender-widget.js",
11483
- "settings": [
11484
- {
11485
- "type": "text",
11486
- "id": "element_tag",
11487
- "label": "Tender custom element",
11488
- "default": ${JSON.stringify(tenderElement)}
11489
- },
11490
- {
11491
- "type": "text",
11492
- "id": "artifact_id",
11493
- "label": "Tender artifact ID",
11494
- "default": ${JSON.stringify(artifactId)}
11495
- },
11496
- {
11497
- "type": "text",
11498
- "id": "public_id",
11499
- "label": "Tender public ID",
11500
- "default": ${JSON.stringify(publicId)}
11501
- },
11502
- {
11503
- "type": "select",
11504
- "id": "widget_version",
11505
- "label": "Widget version",
11506
- "options": [
11507
- { "value": "latest", "label": "Latest published version" }
11508
- ],
11509
- "default": "latest"
11510
- },
11511
- {
11512
- "type": "inline_richtext",
11513
- "id": "heading",
11514
- "label": "Heading",
11515
- "default": "Tell us about your customer"
11516
- },
11517
- {
11518
- "type": "text",
11519
- "id": "intro",
11520
- "label": "Intro",
11521
- "default": "Answer a few quick questions and keep your profile up to date."
11522
- },
11523
- {
11524
- "type": "select",
11525
- "id": "tone",
11526
- "label": "Tone",
11527
- "options": [
11528
- { "value": "default", "label": "Default" },
11529
- { "value": "subtle", "label": "Subtle" },
11530
- { "value": "contrast", "label": "Contrast" }
11531
- ],
11532
- "default": "default"
11533
- }
11534
- ]
11535
- }
11536
- {% endschema %}
11537
11258
  `;
11538
11259
  const readme = `# ${appName}
11539
11260
 
11540
- This is the merchant-owned Shopify app source generated by Tender Prompt for
11261
+ This is the merchant-owned Shopify app source bootstrapped by Tender Prompt for
11541
11262
  connector \`${input.connectorId}\`.
11542
11263
 
11543
11264
  ## What lives here
11544
11265
 
11545
- - \`shopify.app.toml\`: Shopify app configuration, scopes, and app-owned dog-profile custom data definitions.
11546
- - \`extensions/${extensionHandle}\`: Theme App Extension with a Tender app block.
11547
- - \`connector.json\`: Non-secret Tender metadata that maps this source tree back to the connector.
11266
+ - \`shopify.app.toml\`: Shopify app configuration and reviewed Admin API scopes.
11267
+ - \`connector.json\`: Non-secret Tender metadata that maps this source tree back
11268
+ to the connector.
11269
+
11270
+ ## Add capabilities
11271
+
11272
+ This bootstrap intentionally does not generate app blocks, web pixels, customer
11273
+ metadata definitions, or app-owned GraphQL resources. Add those as source edits
11274
+ using the Shopify connector playbooks so the source tree stays the durable
11275
+ record of what the merchant-owned app actually contains.
11548
11276
 
11549
11277
  ## Secrets
11550
11278
 
11551
11279
  Do not commit the Shopify client secret, Admin API token, or App Automation
11552
11280
  Token. Runtime credentials stay in the Tender connector secret store. Deployment
11553
- uses \`SHOPIFY_APP_AUTOMATION_TOKEN\` at command time.
11281
+ uses the connector's stored App Automation Token and passes it only to the
11282
+ spawned Shopify CLI child process.
11554
11283
 
11555
11284
  ## Validate
11556
11285
 
@@ -11561,21 +11290,16 @@ npx --yes @shopify/cli@latest app config validate --path . --json
11561
11290
  ## Deploy
11562
11291
 
11563
11292
  \`\`\`sh
11564
- SHOPIFY_APP_AUTOMATION_TOKEN=... npx --yes @shopify/cli@latest app deploy --path . --allow-updates --version tender-widget
11293
+ tender connectors shopify source deploy ${input.connectorId} --dir . --confirm deploy --profile account --json
11565
11294
  \`\`\`
11566
11295
  `;
11567
11296
  return {
11568
- extensionHandle,
11569
11297
  manifest: connectorManifest,
11570
11298
  files: new Map([
11571
11299
  ['.gitignore', '.shopify/\nnode_modules/\n.env\n.env.*\n'],
11572
11300
  ['README.md', readme],
11573
11301
  ['connector.json', `${JSON.stringify(connectorManifest, null, 2)}\n`],
11574
11302
  ['shopify.app.toml', shopifyAppToml],
11575
- [`extensions/${extensionHandle}/shopify.extension.toml`, extensionToml],
11576
- [`extensions/${extensionHandle}/blocks/tender_widget.liquid`, liquid],
11577
- [`extensions/${extensionHandle}/assets/tender-widget.js`, tenderWidgetAssetJs()],
11578
- [`extensions/${extensionHandle}/locales/en.default.json`, `${JSON.stringify({ tender: { block: { name: blockName } } }, null, 2)}\n`],
11579
11303
  ]),
11580
11304
  };
11581
11305
  }
@@ -11648,7 +11372,6 @@ async function patchConnectorSourceProject(input) {
11648
11372
  version: 1,
11649
11373
  kind: 'shopify_app_source',
11650
11374
  sourceArtifactId: input.sourceArtifactId,
11651
- template: input.template,
11652
11375
  updatedAt: input.updatedAt,
11653
11376
  };
11654
11377
  const payload = await requestConnectorsApi({
@@ -11835,13 +11558,117 @@ function shopifyDeployArgs(input) {
11835
11558
  function commandLine(command, args) {
11836
11559
  return [command, ...args.map(shellQuote)].join(' ');
11837
11560
  }
11561
+ function redactSensitiveText(value, secrets = []) {
11562
+ let redacted = value;
11563
+ for (const secret of secrets) {
11564
+ if (secret) {
11565
+ redacted = redacted.split(secret).join('[REDACTED]');
11566
+ }
11567
+ }
11568
+ return redacted
11569
+ .replace(/shpat_[A-Za-z0-9_:-]+/g, 'shpat_[REDACTED]')
11570
+ .replace(/\b(SHOPIFY_APP_AUTOMATION_TOKEN\s*[=:]\s*)([^\s'"]+)/gi, '$1[REDACTED]')
11571
+ .replace(/\b(authorization\s*[:=]\s*(?:Bearer\s+)?)([^\s'"]+)/gi, '$1[REDACTED]')
11572
+ .replace(/\b(x-shopify-access-token\s*[:=]\s*)([^\s'"]+)/gi, '$1[REDACTED]');
11573
+ }
11838
11574
  async function runNpxCommand(input) {
11839
11575
  const runner = input.runtime.commandRunner ?? defaultCommandRunner;
11840
- const result = await runner('npx', input.args, { cwd: input.cwd });
11841
- if (result.exitCode !== 0) {
11842
- throw new Error([`npx ${input.args.join(' ')} failed with exit code ${result.exitCode}.`, result.stderr.trim() || result.stdout.trim()].filter(Boolean).join('\n'));
11576
+ const result = await runner('npx', input.args, {
11577
+ cwd: input.cwd,
11578
+ ...(input.env ? { env: input.env } : {}),
11579
+ });
11580
+ const redactedResult = {
11581
+ ...result,
11582
+ stdout: redactSensitiveText(result.stdout, input.redactions),
11583
+ stderr: redactSensitiveText(result.stderr, input.redactions),
11584
+ };
11585
+ if (redactedResult.exitCode !== 0) {
11586
+ throw new Error([
11587
+ `npx ${input.args.join(' ')} failed with exit code ${redactedResult.exitCode}.`,
11588
+ redactedResult.stderr.trim() || redactedResult.stdout.trim(),
11589
+ ]
11590
+ .filter(Boolean)
11591
+ .join('\n'));
11592
+ }
11593
+ return redactedResult;
11594
+ }
11595
+ async function fetchShopifyDeployToken(input) {
11596
+ const payload = await requestConnectorsApi({
11597
+ connectorsPath: `${encodeURIComponent(input.connectorId)}/shopify/deploy-token`,
11598
+ method: 'POST',
11599
+ baseUrl: input.credentials.baseUrl,
11600
+ token: input.credentials.token,
11601
+ fetcher: input.fetcher,
11602
+ body: {
11603
+ sourceArtifactId: input.sourceArtifactId,
11604
+ ...(input.version ? { version: input.version } : {}),
11605
+ },
11606
+ });
11607
+ if (typeof payload.token !== 'string' || !payload.token.trim()) {
11608
+ throw new TenderCliUsageError('Tender did not return a Shopify deploy token. Re-run connector setup or store an App Automation Token.');
11609
+ }
11610
+ return {
11611
+ token: payload.token.trim(),
11612
+ expiresAt: typeof payload.expiresAt === 'number' ? payload.expiresAt : null,
11613
+ };
11614
+ }
11615
+ function localShopifyDeployToken(runtime) {
11616
+ const env = runtime.env ?? process.env;
11617
+ const token = env.SHOPIFY_APP_AUTOMATION_TOKEN;
11618
+ return token?.trim() ? token.trim() : null;
11619
+ }
11620
+ function childShopifyDeployEnv(runtime, token) {
11621
+ return {
11622
+ ...process.env,
11623
+ ...(runtime.env ?? {}),
11624
+ SHOPIFY_APP_AUTOMATION_TOKEN: token,
11625
+ };
11626
+ }
11627
+ function connectorsProfileFlag(command) {
11628
+ return command.profileName ? `--profile ${command.profileName}` : '--profile account';
11629
+ }
11630
+ function shopifyDeployTokenRecoveryMessage(command, reasonCode) {
11631
+ const profileFlag = connectorsProfileFlag(command);
11632
+ return [
11633
+ reasonCode === 'shopify_deploy_token_expired'
11634
+ ? 'The connector stored Shopify App Automation Token is expired.'
11635
+ : 'The connector does not have a stored Shopify App Automation Token for source deploys.',
11636
+ `Store a fresh token: printf "$SHOPIFY_APP_AUTOMATION_TOKEN" | tender connectors shopify app-automation-token set ${command.connectorId} --token-stdin ${profileFlag} --json`,
11637
+ `Setup URL: tender connectors shopify setup-url ${command.connectorId} ${profileFlag} --json`,
11638
+ 'Fallback: export SHOPIFY_APP_AUTOMATION_TOKEN locally before running source deploy.',
11639
+ ].join('\n');
11640
+ }
11641
+ async function resolveShopifyDeployTokenEnvironment(input) {
11642
+ const localToken = localShopifyDeployToken(input.runtime);
11643
+ if (localToken) {
11644
+ return {
11645
+ env: childShopifyDeployEnv(input.runtime, localToken),
11646
+ redactions: [localToken],
11647
+ source: 'local-env',
11648
+ };
11649
+ }
11650
+ try {
11651
+ const handoff = await fetchShopifyDeployToken({
11652
+ connectorId: input.command.connectorId,
11653
+ sourceArtifactId: input.sourceArtifactId,
11654
+ version: input.command.version,
11655
+ credentials: input.credentials,
11656
+ fetcher: input.fetcher,
11657
+ });
11658
+ return {
11659
+ env: childShopifyDeployEnv(input.runtime, handoff.token),
11660
+ redactions: [handoff.token],
11661
+ source: 'stored-connector-token',
11662
+ };
11663
+ }
11664
+ catch (error) {
11665
+ if (error instanceof TenderCliConnectorsApiError &&
11666
+ (error.reasonCode === 'shopify_deploy_token_required' || error.reasonCode === 'shopify_deploy_token_expired')) {
11667
+ const recovery = shopifyDeployTokenRecoveryMessage(input.command, error.reasonCode);
11668
+ throw new TenderCliGuidedUsageError(recovery, recovery);
11669
+ }
11670
+ throw error;
11843
11671
  }
11844
- return result;
11845
11672
  }
11846
11673
  async function runConnectorsShopifySourceInit(command, io, runtime) {
11847
11674
  const fetcher = runtime.fetcher ?? fetch;
@@ -11865,14 +11692,6 @@ async function runConnectorsShopifySourceInit(command, io, runtime) {
11865
11692
  connectorId: command.connectorId,
11866
11693
  connector,
11867
11694
  clientId: command.clientId,
11868
- template: command.template,
11869
- tenderAppUrl: command.tenderAppUrl,
11870
- tenderElement: command.tenderElement,
11871
- artifactId: command.artifactId,
11872
- publicId: command.publicId,
11873
- themeExtensionHandle: command.themeExtensionHandle,
11874
- themeExtensionUid: command.themeExtensionUid,
11875
- blockName: command.blockName,
11876
11695
  scopes: command.scopes,
11877
11696
  });
11878
11697
  const files = await writeShopifySourceFiles({ dir, files: source.files, force: command.force });
@@ -11887,7 +11706,6 @@ async function runConnectorsShopifySourceInit(command, io, runtime) {
11887
11706
  const patched = await patchConnectorSourceProject({
11888
11707
  connectorId: command.connectorId,
11889
11708
  sourceArtifactId: sourceArtifact.sourceArtifactId,
11890
- template: command.template,
11891
11709
  credentials,
11892
11710
  fetcher,
11893
11711
  updatedAt: runtime.now ? runtime.now() : Date.now(),
@@ -11911,7 +11729,7 @@ async function runConnectorsShopifySourceInit(command, io, runtime) {
11911
11729
  commit: gitCommit,
11912
11730
  },
11913
11731
  validateCommand: commandLine('npx', shopifyValidateArgs({ dir, clientId: command.clientId })),
11914
- deployCommand: commandLine('npx', shopifyDeployArgs({ dir, clientId: command.clientId, version: 'tender-widget', message: null, sourceControlUrl: null, allowDeletes: false })),
11732
+ deployCommand: commandLine('npx', shopifyDeployArgs({ dir, clientId: command.clientId, version: 'connector-source', message: null, sourceControlUrl: null, allowDeletes: false })),
11915
11733
  };
11916
11734
  printConnectorsPayload(command, io, result, [
11917
11735
  `connector_id: ${command.connectorId}`,
@@ -11920,6 +11738,7 @@ async function runConnectorsShopifySourceInit(command, io, runtime) {
11920
11738
  `files: ${files.length}`,
11921
11739
  `git_pushed: ${gitCommit.pushed ? 'true' : 'false'}`,
11922
11740
  `next_validate: ${result.validateCommand}`,
11741
+ 'next_source_edits: add app blocks, pixels, or metadata definitions from playbooks',
11923
11742
  ]);
11924
11743
  return 0;
11925
11744
  }
@@ -11980,7 +11799,8 @@ async function runConnectorsShopifySourceValidate(command, io, runtime) {
11980
11799
  return 0;
11981
11800
  }
11982
11801
  async function runConnectorsShopifySourceDeploy(command, io, runtime) {
11983
- const { connector } = await requestConnector(command, runtime);
11802
+ const fetcher = runtime.fetcher ?? fetch;
11803
+ const { credentials, connector } = await requestConnector(command, runtime);
11984
11804
  const sourceArtifactId = readConnectorSourceArtifactId(connector);
11985
11805
  if (!sourceArtifactId) {
11986
11806
  throw new TenderCliUsageError('Connector does not have a source artifact yet. Run tender connectors shopify source init <connector-id> --dir ./shopify-app first.');
@@ -11998,7 +11818,14 @@ async function runConnectorsShopifySourceDeploy(command, io, runtime) {
11998
11818
  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']);
11999
11819
  return 0;
12000
11820
  }
12001
- const result = await runNpxCommand({ args, cwd: dir, runtime });
11821
+ const deployToken = await resolveShopifyDeployTokenEnvironment({
11822
+ command,
11823
+ sourceArtifactId,
11824
+ credentials,
11825
+ runtime,
11826
+ fetcher,
11827
+ });
11828
+ const result = await runNpxCommand({ args, cwd: dir, runtime, env: deployToken.env, redactions: deployToken.redactions });
12002
11829
  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']);
12003
11830
  return 0;
12004
11831
  }
@@ -12841,7 +12668,7 @@ function cliErrorMessage(error) {
12841
12668
  return String(error);
12842
12669
  }
12843
12670
  function cliErrorCode(error) {
12844
- if (error instanceof TenderCliDbApiError || error instanceof TenderCliAnalyticsApiError) {
12671
+ if (error instanceof TenderCliDbApiError || error instanceof TenderCliAnalyticsApiError || error instanceof TenderCliConnectorsApiError) {
12845
12672
  return error.reasonCode;
12846
12673
  }
12847
12674
  if (error && typeof error === 'object' && 'code' in error) {
@@ -12916,6 +12743,9 @@ function exampleForError(error, args, command) {
12916
12743
  return dbPath ? dbCorrectedExample(artifactId, dbPath) : null;
12917
12744
  }
12918
12745
  function nextActionForError(error) {
12746
+ if (error instanceof TenderCliGuidedUsageError) {
12747
+ return error.nextAction;
12748
+ }
12919
12749
  return error instanceof TenderCliDbApiError ? error.nextAction : null;
12920
12750
  }
12921
12751
  function cliErrorPayload(error, args, command) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenderprompt/cli",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "tender": "bin/tender.js"
@@ -23,7 +23,7 @@
23
23
  "prepack": "yarn build"
24
24
  },
25
25
  "dependencies": {
26
- "@tenderprompt/tender-app-validator": "0.1.4"
26
+ "@tenderprompt/tender-app-validator": "0.1.5"
27
27
  },
28
28
  "engines": {
29
29
  "node": ">=20"