@tenderprompt/cli 0.1.27 → 0.1.29

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/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;
@@ -433,9 +453,9 @@ Examples:
433
453
  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
454
  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
455
  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
437
- tender connectors shopify source validate --dir ./shopify-app --execute --json
438
- SHOPIFY_APP_AUTOMATION_TOKEN=shpat_... tender connectors shopify source deploy --dir ./shopify-app --confirm deploy --json
456
+ tender connectors shopify source init conn_123 --dir ./shopify-app --client-id abc --profile account --json
457
+ tender connectors shopify source validate conn_123 --dir ./shopify-app --execute --json
458
+ tender connectors shopify source deploy conn_123 --dir ./shopify-app --confirm deploy --profile account --json
439
459
  tender connectors bind SHOPIFY_CONNECTOR conn_123 --instance default --json`;
440
460
  }
441
461
  function connectorsShopifyCreateHelp() {
@@ -493,16 +513,19 @@ Examples:
493
513
  tender connectors shopify verify conn_123 --profile account --json`;
494
514
  }
495
515
  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]
516
+ 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
517
 
498
518
  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.
519
+ Shopify app, checks it out to --dir, writes a minimal non-secret Shopify app
520
+ source tree, commits it, pushes it to Artifact Git, and records
521
+ sourceArtifactId on the connector.
522
+
523
+ The CLI bootstraps and deploys source. App blocks, web pixels, customer
524
+ metadata definitions, and app-owned GraphQL resources are source edits guided by
525
+ playbooks, not source init flags.
503
526
 
504
527
  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`;
528
+ tender connectors shopify source init conn_123 --dir ./shopify-app --client-id abc --profile account --json`;
506
529
  }
507
530
  function connectorsShopifySourceCheckoutHelp() {
508
531
  return `Usage: tender connectors shopify source checkout <connector-id> --dir <path> [--remote <name>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
@@ -526,11 +549,13 @@ function connectorsShopifySourceDeployHelp() {
526
549
  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
550
 
528
551
  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.
552
+ --confirm deploy, runs npx --yes @shopify/cli@latest app deploy. If
553
+ SHOPIFY_APP_AUTOMATION_TOKEN is not set locally, the CLI requests the
554
+ connector's stored App Automation Token and injects it only into the spawned
555
+ Shopify CLI child process.
531
556
 
532
557
  Examples:
533
- SHOPIFY_APP_AUTOMATION_TOKEN=shpat_... tender connectors shopify source deploy conn_123 --dir ./shopify-app --version dog-profile-smoke --confirm deploy --json`;
558
+ tender connectors shopify source deploy conn_123 --dir ./shopify-app --version connector-source-smoke --confirm deploy --profile account --json`;
534
559
  }
535
560
  function connectorsShopifyDeleteHelp() {
536
561
  return `Usage: tender connectors shopify delete <connector-id> --confirm delete [--base-url <url>] [--token <token>] [--profile <name>] [--json]
@@ -1476,6 +1501,51 @@ function tenderCliCapabilities() {
1476
1501
  persistence: 'session_only',
1477
1502
  writesToCheckout: false,
1478
1503
  },
1504
+ connectors: {
1505
+ mode: 'tenant_owned_platform_bindings',
1506
+ listCommand: 'tender connectors list --profile account --json',
1507
+ bindCommand: 'tender connectors bind <binding-name> <connector-id> --instance <instance-id> --json',
1508
+ shopify: {
1509
+ playbookId: 'shopify-connector-custom-app',
1510
+ playbookCommand: 'tender playbooks get shopify-connector-custom-app --json',
1511
+ customerMetadataPlaybookCommand: 'tender playbooks get shopify-connector-customer-metadata-widget --json',
1512
+ whatItIs: 'A merchant-owned Shopify custom app exposed to Tender apps through an explicit platform connector binding.',
1513
+ whenToUse: [
1514
+ 'Shopify Admin GraphQL reads or writes',
1515
+ 'customer metafields or customer-linked metaobjects',
1516
+ 'app-owned custom data definitions',
1517
+ 'Theme App Extension app blocks',
1518
+ 'privileged background Shopify work that browser code cannot safely perform',
1519
+ ],
1520
+ whenNotToUse: [
1521
+ 'pure frontend widgets',
1522
+ 'Tender analytics only',
1523
+ 'public Shopify data already present on the page',
1524
+ 'client-side event publishing without Admin API writes',
1525
+ ],
1526
+ binding: {
1527
+ name: 'SHOPIFY_CONNECTOR',
1528
+ source: 'platform:shopify_connector',
1529
+ interface: 'ShopifyConnector',
1530
+ callShape: 'await this.env.SHOPIFY_CONNECTOR.invoke("shopify.graphql", { query, variables })',
1531
+ },
1532
+ sourceProject: {
1533
+ durableIdentity: 'connector.sourceProject.sourceArtifactId',
1534
+ targetPackId: 'shopify-connector-source',
1535
+ visibility: 'internal_source_artifact_not_customer_facing_app',
1536
+ },
1537
+ sourceModel: {
1538
+ cliOwns: ['source artifact lifecycle', 'checkout', 'Shopify CLI validate/deploy forwarding'],
1539
+ playbooksOwn: ['Theme App Extension app blocks', 'web pixels', 'customer metadata definitions', 'Admin GraphQL resource shape'],
1540
+ },
1541
+ guardrails: [
1542
+ 'Use the account profile because connectors are account-level resources.',
1543
+ 'Use minimum Shopify scopes for the requested job.',
1544
+ 'Never commit client secrets, Admin API tokens, app automation tokens, Tender tokens, or customer data exports.',
1545
+ 'Do not create bindings/<id>/ files for Shopify connectors; use the platform connector source in app.json.',
1546
+ ],
1547
+ },
1548
+ },
1479
1549
  analyticsAuthoring: {
1480
1550
  command: ANALYTICS_AUTHORING_COMMAND,
1481
1551
  playbookId: ANALYTICS_AUTHORING_PLAYBOOK_ID,
@@ -1502,6 +1572,8 @@ function tenderCliCapabilities() {
1502
1572
  'tender app db --help',
1503
1573
  'tender app bindings outbound --help',
1504
1574
  'tender app outbound-auth --help',
1575
+ 'tender connectors --help',
1576
+ 'tender connectors shopify source init --help',
1505
1577
  ],
1506
1578
  },
1507
1579
  workflows: [
@@ -1527,6 +1599,24 @@ function tenderCliCapabilities() {
1527
1599
  'Do not write fetched playbook contents into the checkout or install them as skills.',
1528
1600
  ],
1529
1601
  },
1602
+ {
1603
+ name: 'shopify_connector_discovery',
1604
+ when: 'When a Tender App needs Shopify Admin API access, customer metadata, app blocks, app-owned custom data, or a merchant-owned Shopify custom app.',
1605
+ commands: [
1606
+ 'tender capabilities --json',
1607
+ 'tender playbooks list --json',
1608
+ 'tender playbooks get shopify-connector-custom-app --json',
1609
+ 'tender connectors list --profile account --json',
1610
+ 'tender connectors bind SHOPIFY_CONNECTOR <connector-id> --instance default --json',
1611
+ ],
1612
+ notes: [
1613
+ 'Connectors are tenant-owned platform bindings, not generated binding workers.',
1614
+ 'The merchant owns the Shopify custom app; Tender stores connector credentials and app source metadata.',
1615
+ 'Use connector.sourceProject.sourceArtifactId as the durable Shopify app source identity.',
1616
+ 'Use source init for bootstrap only; app blocks, pixels, metadata definitions, and Admin GraphQL operations are source edits guided by playbooks.',
1617
+ 'Fetch shopify-connector-customer-metadata-widget when the requested job involves customer metafields, metaobjects, or profile writes.',
1618
+ ],
1619
+ },
1530
1620
  {
1531
1621
  name: 'create_new_app_with_preview',
1532
1622
  when: 'When the user wants a new Tender App that is immediately visible.',
@@ -3500,18 +3590,11 @@ function parseConnectorsShopifySourceInitArgs(args) {
3500
3590
  if (args.includes('--help') || args.includes('-h')) {
3501
3591
  return { command: 'help', topic: 'connectors shopify source init' };
3502
3592
  }
3503
- 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');
3593
+ const connectorId = parseConnectorId(args[0], 'tender connectors shopify source init <connector-id> --dir ./shopify-app --client-id abc --json');
3504
3594
  const parsed = parseConnectorsCommonArgs(args, 1);
3505
3595
  let dir = null;
3506
3596
  let clientId = null;
3507
3597
  let sourceName = null;
3508
- let tenderAppUrl = null;
3509
- let tenderElement = null;
3510
- let artifactId = null;
3511
- let publicId = null;
3512
- let themeExtensionHandle = 'tender-widget';
3513
- let themeExtensionUid = null;
3514
- let blockName = 'Tender Prompt Widget';
3515
3598
  let scopes = null;
3516
3599
  let force = false;
3517
3600
  for (let index = 0; index < parsed.remaining.length; index += 1) {
@@ -3531,41 +3614,6 @@ function parseConnectorsShopifySourceInitArgs(args) {
3531
3614
  index += 1;
3532
3615
  continue;
3533
3616
  }
3534
- if (arg === '--tender-app-url') {
3535
- tenderAppUrl = parseRequiredFlagValue(parsed.remaining[index + 1], '--tender-app-url');
3536
- index += 1;
3537
- continue;
3538
- }
3539
- if (arg === '--tender-element') {
3540
- tenderElement = parseRequiredFlagValue(parsed.remaining[index + 1], '--tender-element');
3541
- index += 1;
3542
- continue;
3543
- }
3544
- if (arg === '--artifact-id') {
3545
- artifactId = parseRequiredFlagValue(parsed.remaining[index + 1], '--artifact-id');
3546
- index += 1;
3547
- continue;
3548
- }
3549
- if (arg === '--public-id') {
3550
- publicId = parseRequiredFlagValue(parsed.remaining[index + 1], '--public-id');
3551
- index += 1;
3552
- continue;
3553
- }
3554
- if (arg === '--theme-extension-handle') {
3555
- themeExtensionHandle = parseRequiredFlagValue(parsed.remaining[index + 1], '--theme-extension-handle');
3556
- index += 1;
3557
- continue;
3558
- }
3559
- if (arg === '--theme-extension-uid') {
3560
- themeExtensionUid = parseRequiredFlagValue(parsed.remaining[index + 1], '--theme-extension-uid');
3561
- index += 1;
3562
- continue;
3563
- }
3564
- if (arg === '--block-name') {
3565
- blockName = parseRequiredFlagValue(parsed.remaining[index + 1], '--block-name');
3566
- index += 1;
3567
- continue;
3568
- }
3569
3617
  if (arg === '--scopes') {
3570
3618
  scopes = parseRequiredFlagValue(parsed.remaining[index + 1], '--scopes');
3571
3619
  index += 1;
@@ -3578,31 +3626,17 @@ function parseConnectorsShopifySourceInitArgs(args) {
3578
3626
  throw new TenderCliUsageError(`Unknown connectors shopify source init option: ${arg}`);
3579
3627
  }
3580
3628
  if (!dir) {
3581
- 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');
3629
+ throw new TenderCliUsageError('--dir is required. Example: tender connectors shopify source init conn_123 --dir ./shopify-app --client-id abc --json');
3582
3630
  }
3583
3631
  if (!clientId) {
3584
3632
  throw new TenderCliUsageError('--client-id is required because Tender stores only the non-secret client ID prefix.');
3585
3633
  }
3586
- if (!tenderAppUrl) {
3587
- throw new TenderCliUsageError('--tender-app-url is required. Pass the Tender app client.js URL to load in the app block.');
3588
- }
3589
- if (!tenderElement) {
3590
- throw new TenderCliUsageError('--tender-element is required. Pass the Tender custom element tag to mount in Shopify.');
3591
- }
3592
3634
  return {
3593
3635
  command: 'connectors shopify source init',
3594
3636
  connectorId,
3595
3637
  dir,
3596
3638
  clientId,
3597
3639
  sourceName,
3598
- template: 'dog-profile',
3599
- tenderAppUrl,
3600
- tenderElement,
3601
- artifactId,
3602
- publicId,
3603
- themeExtensionHandle,
3604
- themeExtensionUid,
3605
- blockName,
3606
3640
  scopes,
3607
3641
  force,
3608
3642
  ...parsed,
@@ -7502,7 +7536,7 @@ async function requestConnectorsApi(input) {
7502
7536
  const message = (typeof payload?.message === 'string' && payload.message) ||
7503
7537
  (typeof payload?.reasonCode === 'string' && payload.reasonCode) ||
7504
7538
  `Connector request failed with HTTP ${response.status}.`;
7505
- throw new Error([message, `Reason: ${reasonCode}.`, 'Example: tender connectors list --profile account --json.'].join('\n'));
7539
+ throw new TenderCliConnectorsApiError([message, `Reason: ${reasonCode}.`, 'Example: tender connectors list --profile account --json.'].join('\n'), reasonCode, response.status, payload ?? null);
7506
7540
  }
7507
7541
  return payload;
7508
7542
  }
@@ -8898,6 +8932,7 @@ function defaultCommandRunner(command, args, options) {
8898
8932
  return new Promise((resolve, reject) => {
8899
8933
  const child = spawn(command, args, {
8900
8934
  cwd: options.cwd,
8935
+ ...(options.env ? { env: options.env } : {}),
8901
8936
  stdio: ['ignore', 'pipe', 'pipe'],
8902
8937
  });
8903
8938
  const stdout = [];
@@ -11076,20 +11111,6 @@ async function runConnectorsShopifyVerify(command, io, runtime) {
11076
11111
  function requireShopifySourceString(value, fallback = '') {
11077
11112
  return typeof value === 'string' && value.trim() ? value.trim() : fallback;
11078
11113
  }
11079
- function normalizeTenderElementTag(value) {
11080
- const normalized = value.trim().toLowerCase();
11081
- if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)+$/.test(normalized)) {
11082
- throw new TenderCliUsageError('--tender-element must be a valid custom element tag, for example tender-dog-profile.');
11083
- }
11084
- return normalized;
11085
- }
11086
- function normalizeThemeExtensionHandle(value) {
11087
- const normalized = value.trim().toLowerCase();
11088
- if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(normalized)) {
11089
- throw new TenderCliUsageError('--theme-extension-handle must contain only lowercase letters, numbers, and dashes.');
11090
- }
11091
- return normalized;
11092
- }
11093
11114
  function tomlString(value) {
11094
11115
  return JSON.stringify(value);
11095
11116
  }
@@ -11099,107 +11120,30 @@ function uniqueSortedScopes(scopes) {
11099
11120
  function parseScopeCsv(value) {
11100
11121
  return uniqueSortedScopes((value ?? '').split(/[,\s]+/));
11101
11122
  }
11102
- function defaultDogProfileScopes(connectorScopes, override) {
11123
+ function defaultShopifySourceScopes(connectorScopes, override) {
11103
11124
  if (override) {
11104
11125
  return parseScopeCsv(override);
11105
11126
  }
11106
11127
  const fromConnector = Array.isArray(connectorScopes) ? connectorScopes.filter((scope) => typeof scope === 'string') : [];
11107
- return uniqueSortedScopes([
11108
- ...fromConnector,
11109
- 'read_customers',
11110
- 'write_customers',
11111
- 'read_metaobject_definitions',
11112
- 'write_metaobject_definitions',
11113
- 'read_metaobjects',
11114
- 'write_metaobjects',
11115
- ]);
11116
- }
11117
- function tenderWidgetAssetJs() {
11118
- return `(function () {
11119
- const loader = (window.TenderPromptBlockLoader ||= { scripts: new Map() });
11120
-
11121
- function loadScript(src) {
11122
- if (!src) return Promise.reject(new Error("Missing Tender runtime URL"));
11123
-
11124
- const existing = loader.scripts.get(src);
11125
- if (existing) return existing.promise;
11126
-
11127
- const promise = new Promise((resolve, reject) => {
11128
- const script = document.createElement("script");
11129
- script.type = "module";
11130
- script.src = src;
11131
- script.onload = resolve;
11132
- script.onerror = () => reject(new Error(\`Failed to load \${src}\`));
11133
- document.head.appendChild(script);
11134
- });
11135
-
11136
- loader.scripts.set(src, { status: "loading", promise });
11137
- promise.then(
11138
- () => loader.scripts.set(src, { status: "loaded", promise }),
11139
- () => loader.scripts.delete(src),
11140
- );
11141
-
11142
- return promise;
11143
- }
11144
-
11145
- class TenderPromptWidgetBlock extends HTMLElement {
11146
- connectedCallback() {
11147
- this.mount();
11148
- }
11149
-
11150
- async mount() {
11151
- const mount = this.querySelector("[data-tender-mount]");
11152
- const elementTag = this.dataset.tenderElementTag;
11153
- if (!mount || !elementTag || mount.dataset.tenderMounted === "true") return;
11154
-
11155
- await loadScript(this.dataset.tenderRuntimeSrc);
11156
-
11157
- const widget = document.createElement(elementTag);
11158
- for (const [key, value] of Object.entries(this.dataset)) {
11159
- if (value == null || key === "tenderRuntimeSrc" || key === "tenderElementTag") continue;
11160
- widget.dataset[key] = value;
11161
- }
11162
-
11163
- mount.replaceChildren(widget);
11164
- mount.dataset.tenderMounted = "true";
11165
- }
11166
- }
11167
-
11168
- if (!customElements.get("tender-prompt-widget-block")) {
11169
- customElements.define("tender-prompt-widget-block", TenderPromptWidgetBlock);
11170
- }
11171
- })();
11172
- `;
11128
+ return uniqueSortedScopes(fromConnector);
11173
11129
  }
11174
11130
  function buildShopifySourceFiles(input) {
11175
11131
  const appName = requireShopifySourceString(input.connector.appName, 'Tender Prompt Apps');
11176
11132
  const adminApiVersion = requireShopifySourceString(input.connector.adminApiVersion, '2026-04');
11177
11133
  const shopDomain = requireShopifySourceString(input.connector.shopDomain);
11178
- const tenderElement = normalizeTenderElementTag(input.tenderElement);
11179
- const extensionHandle = normalizeThemeExtensionHandle(input.themeExtensionHandle);
11180
- const scopes = defaultDogProfileScopes(input.connector.scopes, input.scopes);
11181
- const artifactId = input.artifactId ?? 'artifact_replace_me';
11182
- const publicId = input.publicId ?? 'tender-widget';
11183
- const blockName = input.blockName.trim().slice(0, 120) || 'Tender Prompt Widget';
11134
+ const scopes = defaultShopifySourceScopes(input.connector.scopes, input.scopes);
11184
11135
  const connectorManifest = {
11185
11136
  version: 1,
11186
11137
  kind: 'shopify_connector_source',
11187
11138
  connectorId: input.connectorId,
11188
11139
  instanceId: requireShopifySourceString(input.connector.instanceId, 'default'),
11189
- template: input.template,
11190
11140
  shopDomain: shopDomain || null,
11191
11141
  adminApiVersion,
11192
11142
  clientId: input.clientId,
11193
- themeExtensionHandle: extensionHandle,
11194
- tender: {
11195
- artifactId,
11196
- publicId,
11197
- elementTag: tenderElement,
11198
- runtimeUrl: input.tenderAppUrl,
11199
- },
11143
+ scopes,
11200
11144
  secrets: {
11201
11145
  clientSecret: 'stored in Tender connector credentials, not in this source tree',
11202
- appAutomationToken: 'store in SHOPIFY_APP_AUTOMATION_TOKEN or Tender connector secret store, not in git',
11146
+ appAutomationToken: 'stored in Tender connector secret store, not in this source tree',
11203
11147
  },
11204
11148
  };
11205
11149
  const shopifyAppToml = `# Merchant-owned Shopify app source generated by Tender Prompt.
@@ -11221,157 +11165,31 @@ api_version = ${tomlString(adminApiVersion)}
11221
11165
 
11222
11166
  [build]
11223
11167
  automatically_update_urls_on_dev = true
11224
-
11225
- [metaobjects.app.dog_profile]
11226
- name = "Dog profile"
11227
- display_name_field = "dog_name"
11228
- access.admin = "merchant_read_write"
11229
-
11230
- [metaobjects.app.dog_profile.fields.dog_name]
11231
- name = "Dog name"
11232
- type = "single_line_text_field"
11233
- required = true
11234
-
11235
- [metaobjects.app.dog_profile.fields.preferences]
11236
- name = "Preferences"
11237
- type = "json"
11238
-
11239
- [customer.metafields.app.dog_profile]
11240
- name = "Dog profile"
11241
- type = "metaobject_reference<$app:dog_profile>"
11242
- access.admin = "merchant_read_write"
11243
- `;
11244
- const extensionToml = [
11245
- `name = ${tomlString(extensionHandle)}`,
11246
- 'type = "theme"',
11247
- input.themeExtensionUid ? `uid = ${tomlString(input.themeExtensionUid)}` : null,
11248
- '',
11249
- ]
11250
- .filter((line) => line !== null)
11251
- .join('\n');
11252
- const liquid = `{% doc %}
11253
- Tender Prompt app block generated from connector ${input.connectorId}.
11254
-
11255
- The block loads one Tender runtime script and mounts the configured Tender
11256
- custom element. Keep merchant-editable copy and version pins in block settings.
11257
- {% enddoc %}
11258
-
11259
- {% liquid
11260
- assign artifact_id = block.settings.artifact_id | default: ${tomlString(artifactId)}
11261
- assign runtime_src = block.settings.runtime_src | default: ${tomlString(input.tenderAppUrl)}
11262
- assign widget_version = block.settings.widget_version | default: 'latest'
11263
- %}
11264
-
11265
- <tender-prompt-widget-block
11266
- {{ block.shopify_attributes }}
11267
- class="tender-prompt-widget-block tender-prompt-widget-block--{{ block.settings.tone }}"
11268
- data-tender-artifact-id="{{ artifact_id | escape }}"
11269
- data-tender-public-id="{{ block.settings.public_id | escape }}"
11270
- data-tender-version="{{ widget_version | escape }}"
11271
- data-tender-runtime-src="{{ runtime_src | escape }}"
11272
- data-tender-element-tag="{{ block.settings.element_tag | escape }}"
11273
- data-tender-mode="shopify_app_block"
11274
- data-surface="shopify_theme_app_extension"
11275
- data-tender-block-id="{{ block.id | escape }}"
11276
- data-tender-shop-domain="{{ shop.permanent_domain | escape }}"
11277
- data-tender-heading="{{ block.settings.heading | escape }}"
11278
- data-tender-intro="{{ block.settings.intro | escape }}"
11279
- data-tender-tone="{{ block.settings.tone | escape }}"
11280
- {% if customer %}
11281
- data-tender-customer-id="{{ customer.id }}"
11282
- {% endif %}
11283
- {% if product %}
11284
- data-tender-product-id="{{ product.id }}"
11285
- data-tender-product-handle="{{ product.handle | escape }}"
11286
- {% endif %}
11287
- >
11288
- <div data-tender-mount>
11289
- <p>{{ block.settings.heading }}</p>
11290
- </div>
11291
- </tender-prompt-widget-block>
11292
-
11293
- {% schema %}
11294
- {
11295
- "name": ${JSON.stringify(blockName)},
11296
- "target": "section",
11297
- "javascript": "tender-widget.js",
11298
- "settings": [
11299
- {
11300
- "type": "text",
11301
- "id": "runtime_src",
11302
- "label": "Tender runtime URL",
11303
- "default": ${JSON.stringify(input.tenderAppUrl)}
11304
- },
11305
- {
11306
- "type": "text",
11307
- "id": "element_tag",
11308
- "label": "Tender custom element",
11309
- "default": ${JSON.stringify(tenderElement)}
11310
- },
11311
- {
11312
- "type": "text",
11313
- "id": "artifact_id",
11314
- "label": "Tender artifact ID",
11315
- "default": ${JSON.stringify(artifactId)}
11316
- },
11317
- {
11318
- "type": "text",
11319
- "id": "public_id",
11320
- "label": "Tender public ID",
11321
- "default": ${JSON.stringify(publicId)}
11322
- },
11323
- {
11324
- "type": "select",
11325
- "id": "widget_version",
11326
- "label": "Widget version",
11327
- "options": [
11328
- { "value": "latest", "label": "Latest published version" }
11329
- ],
11330
- "default": "latest"
11331
- },
11332
- {
11333
- "type": "inline_richtext",
11334
- "id": "heading",
11335
- "label": "Heading",
11336
- "default": "Tell us about your customer"
11337
- },
11338
- {
11339
- "type": "text",
11340
- "id": "intro",
11341
- "label": "Intro",
11342
- "default": "Answer a few quick questions and keep your profile up to date."
11343
- },
11344
- {
11345
- "type": "select",
11346
- "id": "tone",
11347
- "label": "Tone",
11348
- "options": [
11349
- { "value": "default", "label": "Default" },
11350
- { "value": "subtle", "label": "Subtle" },
11351
- { "value": "contrast", "label": "Contrast" }
11352
- ],
11353
- "default": "default"
11354
- }
11355
- ]
11356
- }
11357
- {% endschema %}
11358
11168
  `;
11359
11169
  const readme = `# ${appName}
11360
11170
 
11361
- This is the merchant-owned Shopify app source generated by Tender Prompt for
11171
+ This is the merchant-owned Shopify app source bootstrapped by Tender Prompt for
11362
11172
  connector \`${input.connectorId}\`.
11363
11173
 
11364
11174
  ## What lives here
11365
11175
 
11366
- - \`shopify.app.toml\`: Shopify app configuration, scopes, and app-owned dog-profile custom data definitions.
11367
- - \`extensions/${extensionHandle}\`: Theme App Extension with a Tender app block.
11368
- - \`connector.json\`: Non-secret Tender metadata that maps this source tree back to the connector.
11176
+ - \`shopify.app.toml\`: Shopify app configuration and reviewed Admin API scopes.
11177
+ - \`connector.json\`: Non-secret Tender metadata that maps this source tree back
11178
+ to the connector.
11179
+
11180
+ ## Add capabilities
11181
+
11182
+ This bootstrap intentionally does not generate app blocks, web pixels, customer
11183
+ metadata definitions, or app-owned GraphQL resources. Add those as source edits
11184
+ using the Shopify connector playbooks so the source tree stays the durable
11185
+ record of what the merchant-owned app actually contains.
11369
11186
 
11370
11187
  ## Secrets
11371
11188
 
11372
11189
  Do not commit the Shopify client secret, Admin API token, or App Automation
11373
11190
  Token. Runtime credentials stay in the Tender connector secret store. Deployment
11374
- uses \`SHOPIFY_APP_AUTOMATION_TOKEN\` at command time.
11191
+ uses the connector's stored App Automation Token and passes it only to the
11192
+ spawned Shopify CLI child process.
11375
11193
 
11376
11194
  ## Validate
11377
11195
 
@@ -11382,21 +11200,16 @@ npx --yes @shopify/cli@latest app config validate --path . --json
11382
11200
  ## Deploy
11383
11201
 
11384
11202
  \`\`\`sh
11385
- SHOPIFY_APP_AUTOMATION_TOKEN=... npx --yes @shopify/cli@latest app deploy --path . --allow-updates --version tender-widget
11203
+ tender connectors shopify source deploy ${input.connectorId} --dir . --confirm deploy --profile account --json
11386
11204
  \`\`\`
11387
11205
  `;
11388
11206
  return {
11389
- extensionHandle,
11390
11207
  manifest: connectorManifest,
11391
11208
  files: new Map([
11392
11209
  ['.gitignore', '.shopify/\nnode_modules/\n.env\n.env.*\n'],
11393
11210
  ['README.md', readme],
11394
11211
  ['connector.json', `${JSON.stringify(connectorManifest, null, 2)}\n`],
11395
11212
  ['shopify.app.toml', shopifyAppToml],
11396
- [`extensions/${extensionHandle}/shopify.extension.toml`, extensionToml],
11397
- [`extensions/${extensionHandle}/blocks/tender_widget.liquid`, liquid],
11398
- [`extensions/${extensionHandle}/assets/tender-widget.js`, tenderWidgetAssetJs()],
11399
- [`extensions/${extensionHandle}/locales/en.default.json`, `${JSON.stringify({ tender: { block: { name: blockName } } }, null, 2)}\n`],
11400
11213
  ]),
11401
11214
  };
11402
11215
  }
@@ -11469,7 +11282,6 @@ async function patchConnectorSourceProject(input) {
11469
11282
  version: 1,
11470
11283
  kind: 'shopify_app_source',
11471
11284
  sourceArtifactId: input.sourceArtifactId,
11472
- template: input.template,
11473
11285
  updatedAt: input.updatedAt,
11474
11286
  };
11475
11287
  const payload = await requestConnectorsApi({
@@ -11656,13 +11468,117 @@ function shopifyDeployArgs(input) {
11656
11468
  function commandLine(command, args) {
11657
11469
  return [command, ...args.map(shellQuote)].join(' ');
11658
11470
  }
11471
+ function redactSensitiveText(value, secrets = []) {
11472
+ let redacted = value;
11473
+ for (const secret of secrets) {
11474
+ if (secret) {
11475
+ redacted = redacted.split(secret).join('[REDACTED]');
11476
+ }
11477
+ }
11478
+ return redacted
11479
+ .replace(/shpat_[A-Za-z0-9_:-]+/g, 'shpat_[REDACTED]')
11480
+ .replace(/\b(SHOPIFY_APP_AUTOMATION_TOKEN\s*[=:]\s*)([^\s'"]+)/gi, '$1[REDACTED]')
11481
+ .replace(/\b(authorization\s*[:=]\s*(?:Bearer\s+)?)([^\s'"]+)/gi, '$1[REDACTED]')
11482
+ .replace(/\b(x-shopify-access-token\s*[:=]\s*)([^\s'"]+)/gi, '$1[REDACTED]');
11483
+ }
11659
11484
  async function runNpxCommand(input) {
11660
11485
  const runner = input.runtime.commandRunner ?? defaultCommandRunner;
11661
- const result = await runner('npx', input.args, { cwd: input.cwd });
11662
- if (result.exitCode !== 0) {
11663
- throw new Error([`npx ${input.args.join(' ')} failed with exit code ${result.exitCode}.`, result.stderr.trim() || result.stdout.trim()].filter(Boolean).join('\n'));
11486
+ const result = await runner('npx', input.args, {
11487
+ cwd: input.cwd,
11488
+ ...(input.env ? { env: input.env } : {}),
11489
+ });
11490
+ const redactedResult = {
11491
+ ...result,
11492
+ stdout: redactSensitiveText(result.stdout, input.redactions),
11493
+ stderr: redactSensitiveText(result.stderr, input.redactions),
11494
+ };
11495
+ if (redactedResult.exitCode !== 0) {
11496
+ throw new Error([
11497
+ `npx ${input.args.join(' ')} failed with exit code ${redactedResult.exitCode}.`,
11498
+ redactedResult.stderr.trim() || redactedResult.stdout.trim(),
11499
+ ]
11500
+ .filter(Boolean)
11501
+ .join('\n'));
11502
+ }
11503
+ return redactedResult;
11504
+ }
11505
+ async function fetchShopifyDeployToken(input) {
11506
+ const payload = await requestConnectorsApi({
11507
+ connectorsPath: `${encodeURIComponent(input.connectorId)}/shopify/deploy-token`,
11508
+ method: 'POST',
11509
+ baseUrl: input.credentials.baseUrl,
11510
+ token: input.credentials.token,
11511
+ fetcher: input.fetcher,
11512
+ body: {
11513
+ sourceArtifactId: input.sourceArtifactId,
11514
+ ...(input.version ? { version: input.version } : {}),
11515
+ },
11516
+ });
11517
+ if (typeof payload.token !== 'string' || !payload.token.trim()) {
11518
+ throw new TenderCliUsageError('Tender did not return a Shopify deploy token. Re-run connector setup or store an App Automation Token.');
11519
+ }
11520
+ return {
11521
+ token: payload.token.trim(),
11522
+ expiresAt: typeof payload.expiresAt === 'number' ? payload.expiresAt : null,
11523
+ };
11524
+ }
11525
+ function localShopifyDeployToken(runtime) {
11526
+ const env = runtime.env ?? process.env;
11527
+ const token = env.SHOPIFY_APP_AUTOMATION_TOKEN;
11528
+ return token?.trim() ? token.trim() : null;
11529
+ }
11530
+ function childShopifyDeployEnv(runtime, token) {
11531
+ return {
11532
+ ...process.env,
11533
+ ...(runtime.env ?? {}),
11534
+ SHOPIFY_APP_AUTOMATION_TOKEN: token,
11535
+ };
11536
+ }
11537
+ function connectorsProfileFlag(command) {
11538
+ return command.profileName ? `--profile ${command.profileName}` : '--profile account';
11539
+ }
11540
+ function shopifyDeployTokenRecoveryMessage(command, reasonCode) {
11541
+ const profileFlag = connectorsProfileFlag(command);
11542
+ return [
11543
+ reasonCode === 'shopify_deploy_token_expired'
11544
+ ? 'The connector stored Shopify App Automation Token is expired.'
11545
+ : 'The connector does not have a stored Shopify App Automation Token for source deploys.',
11546
+ `Store a fresh token: printf "$SHOPIFY_APP_AUTOMATION_TOKEN" | tender connectors shopify app-automation-token set ${command.connectorId} --token-stdin ${profileFlag} --json`,
11547
+ `Setup URL: tender connectors shopify setup-url ${command.connectorId} ${profileFlag} --json`,
11548
+ 'Fallback: export SHOPIFY_APP_AUTOMATION_TOKEN locally before running source deploy.',
11549
+ ].join('\n');
11550
+ }
11551
+ async function resolveShopifyDeployTokenEnvironment(input) {
11552
+ const localToken = localShopifyDeployToken(input.runtime);
11553
+ if (localToken) {
11554
+ return {
11555
+ env: childShopifyDeployEnv(input.runtime, localToken),
11556
+ redactions: [localToken],
11557
+ source: 'local-env',
11558
+ };
11559
+ }
11560
+ try {
11561
+ const handoff = await fetchShopifyDeployToken({
11562
+ connectorId: input.command.connectorId,
11563
+ sourceArtifactId: input.sourceArtifactId,
11564
+ version: input.command.version,
11565
+ credentials: input.credentials,
11566
+ fetcher: input.fetcher,
11567
+ });
11568
+ return {
11569
+ env: childShopifyDeployEnv(input.runtime, handoff.token),
11570
+ redactions: [handoff.token],
11571
+ source: 'stored-connector-token',
11572
+ };
11573
+ }
11574
+ catch (error) {
11575
+ if (error instanceof TenderCliConnectorsApiError &&
11576
+ (error.reasonCode === 'shopify_deploy_token_required' || error.reasonCode === 'shopify_deploy_token_expired')) {
11577
+ const recovery = shopifyDeployTokenRecoveryMessage(input.command, error.reasonCode);
11578
+ throw new TenderCliGuidedUsageError(recovery, recovery);
11579
+ }
11580
+ throw error;
11664
11581
  }
11665
- return result;
11666
11582
  }
11667
11583
  async function runConnectorsShopifySourceInit(command, io, runtime) {
11668
11584
  const fetcher = runtime.fetcher ?? fetch;
@@ -11686,14 +11602,6 @@ async function runConnectorsShopifySourceInit(command, io, runtime) {
11686
11602
  connectorId: command.connectorId,
11687
11603
  connector,
11688
11604
  clientId: command.clientId,
11689
- template: command.template,
11690
- tenderAppUrl: command.tenderAppUrl,
11691
- tenderElement: command.tenderElement,
11692
- artifactId: command.artifactId,
11693
- publicId: command.publicId,
11694
- themeExtensionHandle: command.themeExtensionHandle,
11695
- themeExtensionUid: command.themeExtensionUid,
11696
- blockName: command.blockName,
11697
11605
  scopes: command.scopes,
11698
11606
  });
11699
11607
  const files = await writeShopifySourceFiles({ dir, files: source.files, force: command.force });
@@ -11708,7 +11616,6 @@ async function runConnectorsShopifySourceInit(command, io, runtime) {
11708
11616
  const patched = await patchConnectorSourceProject({
11709
11617
  connectorId: command.connectorId,
11710
11618
  sourceArtifactId: sourceArtifact.sourceArtifactId,
11711
- template: command.template,
11712
11619
  credentials,
11713
11620
  fetcher,
11714
11621
  updatedAt: runtime.now ? runtime.now() : Date.now(),
@@ -11732,7 +11639,7 @@ async function runConnectorsShopifySourceInit(command, io, runtime) {
11732
11639
  commit: gitCommit,
11733
11640
  },
11734
11641
  validateCommand: commandLine('npx', shopifyValidateArgs({ dir, clientId: command.clientId })),
11735
- deployCommand: commandLine('npx', shopifyDeployArgs({ dir, clientId: command.clientId, version: 'tender-widget', message: null, sourceControlUrl: null, allowDeletes: false })),
11642
+ deployCommand: commandLine('npx', shopifyDeployArgs({ dir, clientId: command.clientId, version: 'connector-source', message: null, sourceControlUrl: null, allowDeletes: false })),
11736
11643
  };
11737
11644
  printConnectorsPayload(command, io, result, [
11738
11645
  `connector_id: ${command.connectorId}`,
@@ -11741,6 +11648,7 @@ async function runConnectorsShopifySourceInit(command, io, runtime) {
11741
11648
  `files: ${files.length}`,
11742
11649
  `git_pushed: ${gitCommit.pushed ? 'true' : 'false'}`,
11743
11650
  `next_validate: ${result.validateCommand}`,
11651
+ 'next_source_edits: add app blocks, pixels, or metadata definitions from playbooks',
11744
11652
  ]);
11745
11653
  return 0;
11746
11654
  }
@@ -11801,7 +11709,8 @@ async function runConnectorsShopifySourceValidate(command, io, runtime) {
11801
11709
  return 0;
11802
11710
  }
11803
11711
  async function runConnectorsShopifySourceDeploy(command, io, runtime) {
11804
- const { connector } = await requestConnector(command, runtime);
11712
+ const fetcher = runtime.fetcher ?? fetch;
11713
+ const { credentials, connector } = await requestConnector(command, runtime);
11805
11714
  const sourceArtifactId = readConnectorSourceArtifactId(connector);
11806
11715
  if (!sourceArtifactId) {
11807
11716
  throw new TenderCliUsageError('Connector does not have a source artifact yet. Run tender connectors shopify source init <connector-id> --dir ./shopify-app first.');
@@ -11819,7 +11728,14 @@ async function runConnectorsShopifySourceDeploy(command, io, runtime) {
11819
11728
  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']);
11820
11729
  return 0;
11821
11730
  }
11822
- const result = await runNpxCommand({ args, cwd: dir, runtime });
11731
+ const deployToken = await resolveShopifyDeployTokenEnvironment({
11732
+ command,
11733
+ sourceArtifactId,
11734
+ credentials,
11735
+ runtime,
11736
+ fetcher,
11737
+ });
11738
+ const result = await runNpxCommand({ args, cwd: dir, runtime, env: deployToken.env, redactions: deployToken.redactions });
11823
11739
  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']);
11824
11740
  return 0;
11825
11741
  }
@@ -12662,7 +12578,7 @@ function cliErrorMessage(error) {
12662
12578
  return String(error);
12663
12579
  }
12664
12580
  function cliErrorCode(error) {
12665
- if (error instanceof TenderCliDbApiError || error instanceof TenderCliAnalyticsApiError) {
12581
+ if (error instanceof TenderCliDbApiError || error instanceof TenderCliAnalyticsApiError || error instanceof TenderCliConnectorsApiError) {
12666
12582
  return error.reasonCode;
12667
12583
  }
12668
12584
  if (error && typeof error === 'object' && 'code' in error) {
@@ -12737,6 +12653,9 @@ function exampleForError(error, args, command) {
12737
12653
  return dbPath ? dbCorrectedExample(artifactId, dbPath) : null;
12738
12654
  }
12739
12655
  function nextActionForError(error) {
12656
+ if (error instanceof TenderCliGuidedUsageError) {
12657
+ return error.nextAction;
12658
+ }
12740
12659
  return error instanceof TenderCliDbApiError ? error.nextAction : null;
12741
12660
  }
12742
12661
  function cliErrorPayload(error, args, command) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenderprompt/cli",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "tender": "bin/tender.js"