@tenderprompt/cli 0.1.28 → 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
456
+ tender connectors shopify source init conn_123 --dir ./shopify-app --client-id abc --profile account --json
437
457
  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
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]
@@ -1509,6 +1534,10 @@ function tenderCliCapabilities() {
1509
1534
  targetPackId: 'shopify-connector-source',
1510
1535
  visibility: 'internal_source_artifact_not_customer_facing_app',
1511
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
+ },
1512
1541
  guardrails: [
1513
1542
  'Use the account profile because connectors are account-level resources.',
1514
1543
  'Use minimum Shopify scopes for the requested job.',
@@ -1584,6 +1613,7 @@ function tenderCliCapabilities() {
1584
1613
  'Connectors are tenant-owned platform bindings, not generated binding workers.',
1585
1614
  'The merchant owns the Shopify custom app; Tender stores connector credentials and app source metadata.',
1586
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.',
1587
1617
  'Fetch shopify-connector-customer-metadata-widget when the requested job involves customer metafields, metaobjects, or profile writes.',
1588
1618
  ],
1589
1619
  },
@@ -3560,18 +3590,11 @@ function parseConnectorsShopifySourceInitArgs(args) {
3560
3590
  if (args.includes('--help') || args.includes('-h')) {
3561
3591
  return { command: 'help', topic: 'connectors shopify source init' };
3562
3592
  }
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');
3593
+ const connectorId = parseConnectorId(args[0], 'tender connectors shopify source init <connector-id> --dir ./shopify-app --client-id abc --json');
3564
3594
  const parsed = parseConnectorsCommonArgs(args, 1);
3565
3595
  let dir = null;
3566
3596
  let clientId = null;
3567
3597
  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
3598
  let scopes = null;
3576
3599
  let force = false;
3577
3600
  for (let index = 0; index < parsed.remaining.length; index += 1) {
@@ -3591,41 +3614,6 @@ function parseConnectorsShopifySourceInitArgs(args) {
3591
3614
  index += 1;
3592
3615
  continue;
3593
3616
  }
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
3617
  if (arg === '--scopes') {
3630
3618
  scopes = parseRequiredFlagValue(parsed.remaining[index + 1], '--scopes');
3631
3619
  index += 1;
@@ -3638,31 +3626,17 @@ function parseConnectorsShopifySourceInitArgs(args) {
3638
3626
  throw new TenderCliUsageError(`Unknown connectors shopify source init option: ${arg}`);
3639
3627
  }
3640
3628
  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');
3629
+ throw new TenderCliUsageError('--dir is required. Example: tender connectors shopify source init conn_123 --dir ./shopify-app --client-id abc --json');
3642
3630
  }
3643
3631
  if (!clientId) {
3644
3632
  throw new TenderCliUsageError('--client-id is required because Tender stores only the non-secret client ID prefix.');
3645
3633
  }
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
3634
  return {
3653
3635
  command: 'connectors shopify source init',
3654
3636
  connectorId,
3655
3637
  dir,
3656
3638
  clientId,
3657
3639
  sourceName,
3658
- template: 'dog-profile',
3659
- tenderAppUrl,
3660
- tenderElement,
3661
- artifactId,
3662
- publicId,
3663
- themeExtensionHandle,
3664
- themeExtensionUid,
3665
- blockName,
3666
3640
  scopes,
3667
3641
  force,
3668
3642
  ...parsed,
@@ -7562,7 +7536,7 @@ async function requestConnectorsApi(input) {
7562
7536
  const message = (typeof payload?.message === 'string' && payload.message) ||
7563
7537
  (typeof payload?.reasonCode === 'string' && payload.reasonCode) ||
7564
7538
  `Connector request failed with HTTP ${response.status}.`;
7565
- 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);
7566
7540
  }
7567
7541
  return payload;
7568
7542
  }
@@ -8958,6 +8932,7 @@ function defaultCommandRunner(command, args, options) {
8958
8932
  return new Promise((resolve, reject) => {
8959
8933
  const child = spawn(command, args, {
8960
8934
  cwd: options.cwd,
8935
+ ...(options.env ? { env: options.env } : {}),
8961
8936
  stdio: ['ignore', 'pipe', 'pipe'],
8962
8937
  });
8963
8938
  const stdout = [];
@@ -11136,20 +11111,6 @@ async function runConnectorsShopifyVerify(command, io, runtime) {
11136
11111
  function requireShopifySourceString(value, fallback = '') {
11137
11112
  return typeof value === 'string' && value.trim() ? value.trim() : fallback;
11138
11113
  }
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
11114
  function tomlString(value) {
11154
11115
  return JSON.stringify(value);
11155
11116
  }
@@ -11159,213 +11120,30 @@ function uniqueSortedScopes(scopes) {
11159
11120
  function parseScopeCsv(value) {
11160
11121
  return uniqueSortedScopes((value ?? '').split(/[,\s]+/));
11161
11122
  }
11162
- function defaultDogProfileScopes(connectorScopes, override) {
11123
+ function defaultShopifySourceScopes(connectorScopes, override) {
11163
11124
  if (override) {
11164
11125
  return parseScopeCsv(override);
11165
11126
  }
11166
11127
  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
- `;
11128
+ return uniqueSortedScopes(fromConnector);
11337
11129
  }
11338
11130
  function buildShopifySourceFiles(input) {
11339
11131
  const appName = requireShopifySourceString(input.connector.appName, 'Tender Prompt Apps');
11340
11132
  const adminApiVersion = requireShopifySourceString(input.connector.adminApiVersion, '2026-04');
11341
11133
  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';
11134
+ const scopes = defaultShopifySourceScopes(input.connector.scopes, input.scopes);
11349
11135
  const connectorManifest = {
11350
11136
  version: 1,
11351
11137
  kind: 'shopify_connector_source',
11352
11138
  connectorId: input.connectorId,
11353
11139
  instanceId: requireShopifySourceString(input.connector.instanceId, 'default'),
11354
- template: input.template,
11355
11140
  shopDomain: shopDomain || null,
11356
11141
  adminApiVersion,
11357
11142
  clientId: input.clientId,
11358
- themeExtensionHandle: extensionHandle,
11359
- tender: {
11360
- artifactId,
11361
- publicId,
11362
- elementTag: tenderElement,
11363
- runtimeUrl: input.tenderAppUrl,
11364
- stylesheetUrl,
11365
- },
11143
+ scopes,
11366
11144
  secrets: {
11367
11145
  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',
11146
+ appAutomationToken: 'stored in Tender connector secret store, not in this source tree',
11369
11147
  },
11370
11148
  };
11371
11149
  const shopifyAppToml = `# Merchant-owned Shopify app source generated by Tender Prompt.
@@ -11387,170 +11165,31 @@ api_version = ${tomlString(adminApiVersion)}
11387
11165
 
11388
11166
  [build]
11389
11167
  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
11168
  `;
11538
11169
  const readme = `# ${appName}
11539
11170
 
11540
- 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
11541
11172
  connector \`${input.connectorId}\`.
11542
11173
 
11543
11174
  ## What lives here
11544
11175
 
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.
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.
11548
11186
 
11549
11187
  ## Secrets
11550
11188
 
11551
11189
  Do not commit the Shopify client secret, Admin API token, or App Automation
11552
11190
  Token. Runtime credentials stay in the Tender connector secret store. Deployment
11553
- 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.
11554
11193
 
11555
11194
  ## Validate
11556
11195
 
@@ -11561,21 +11200,16 @@ npx --yes @shopify/cli@latest app config validate --path . --json
11561
11200
  ## Deploy
11562
11201
 
11563
11202
  \`\`\`sh
11564
- 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
11565
11204
  \`\`\`
11566
11205
  `;
11567
11206
  return {
11568
- extensionHandle,
11569
11207
  manifest: connectorManifest,
11570
11208
  files: new Map([
11571
11209
  ['.gitignore', '.shopify/\nnode_modules/\n.env\n.env.*\n'],
11572
11210
  ['README.md', readme],
11573
11211
  ['connector.json', `${JSON.stringify(connectorManifest, null, 2)}\n`],
11574
11212
  ['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
11213
  ]),
11580
11214
  };
11581
11215
  }
@@ -11648,7 +11282,6 @@ async function patchConnectorSourceProject(input) {
11648
11282
  version: 1,
11649
11283
  kind: 'shopify_app_source',
11650
11284
  sourceArtifactId: input.sourceArtifactId,
11651
- template: input.template,
11652
11285
  updatedAt: input.updatedAt,
11653
11286
  };
11654
11287
  const payload = await requestConnectorsApi({
@@ -11835,13 +11468,117 @@ function shopifyDeployArgs(input) {
11835
11468
  function commandLine(command, args) {
11836
11469
  return [command, ...args.map(shellQuote)].join(' ');
11837
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
+ }
11838
11484
  async function runNpxCommand(input) {
11839
11485
  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'));
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;
11843
11581
  }
11844
- return result;
11845
11582
  }
11846
11583
  async function runConnectorsShopifySourceInit(command, io, runtime) {
11847
11584
  const fetcher = runtime.fetcher ?? fetch;
@@ -11865,14 +11602,6 @@ async function runConnectorsShopifySourceInit(command, io, runtime) {
11865
11602
  connectorId: command.connectorId,
11866
11603
  connector,
11867
11604
  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
11605
  scopes: command.scopes,
11877
11606
  });
11878
11607
  const files = await writeShopifySourceFiles({ dir, files: source.files, force: command.force });
@@ -11887,7 +11616,6 @@ async function runConnectorsShopifySourceInit(command, io, runtime) {
11887
11616
  const patched = await patchConnectorSourceProject({
11888
11617
  connectorId: command.connectorId,
11889
11618
  sourceArtifactId: sourceArtifact.sourceArtifactId,
11890
- template: command.template,
11891
11619
  credentials,
11892
11620
  fetcher,
11893
11621
  updatedAt: runtime.now ? runtime.now() : Date.now(),
@@ -11911,7 +11639,7 @@ async function runConnectorsShopifySourceInit(command, io, runtime) {
11911
11639
  commit: gitCommit,
11912
11640
  },
11913
11641
  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 })),
11642
+ deployCommand: commandLine('npx', shopifyDeployArgs({ dir, clientId: command.clientId, version: 'connector-source', message: null, sourceControlUrl: null, allowDeletes: false })),
11915
11643
  };
11916
11644
  printConnectorsPayload(command, io, result, [
11917
11645
  `connector_id: ${command.connectorId}`,
@@ -11920,6 +11648,7 @@ async function runConnectorsShopifySourceInit(command, io, runtime) {
11920
11648
  `files: ${files.length}`,
11921
11649
  `git_pushed: ${gitCommit.pushed ? 'true' : 'false'}`,
11922
11650
  `next_validate: ${result.validateCommand}`,
11651
+ 'next_source_edits: add app blocks, pixels, or metadata definitions from playbooks',
11923
11652
  ]);
11924
11653
  return 0;
11925
11654
  }
@@ -11980,7 +11709,8 @@ async function runConnectorsShopifySourceValidate(command, io, runtime) {
11980
11709
  return 0;
11981
11710
  }
11982
11711
  async function runConnectorsShopifySourceDeploy(command, io, runtime) {
11983
- const { connector } = await requestConnector(command, runtime);
11712
+ const fetcher = runtime.fetcher ?? fetch;
11713
+ const { credentials, connector } = await requestConnector(command, runtime);
11984
11714
  const sourceArtifactId = readConnectorSourceArtifactId(connector);
11985
11715
  if (!sourceArtifactId) {
11986
11716
  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 +11728,14 @@ async function runConnectorsShopifySourceDeploy(command, io, runtime) {
11998
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']);
11999
11729
  return 0;
12000
11730
  }
12001
- 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 });
12002
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']);
12003
11740
  return 0;
12004
11741
  }
@@ -12841,7 +12578,7 @@ function cliErrorMessage(error) {
12841
12578
  return String(error);
12842
12579
  }
12843
12580
  function cliErrorCode(error) {
12844
- if (error instanceof TenderCliDbApiError || error instanceof TenderCliAnalyticsApiError) {
12581
+ if (error instanceof TenderCliDbApiError || error instanceof TenderCliAnalyticsApiError || error instanceof TenderCliConnectorsApiError) {
12845
12582
  return error.reasonCode;
12846
12583
  }
12847
12584
  if (error && typeof error === 'object' && 'code' in error) {
@@ -12916,6 +12653,9 @@ function exampleForError(error, args, command) {
12916
12653
  return dbPath ? dbCorrectedExample(artifactId, dbPath) : null;
12917
12654
  }
12918
12655
  function nextActionForError(error) {
12656
+ if (error instanceof TenderCliGuidedUsageError) {
12657
+ return error.nextAction;
12658
+ }
12919
12659
  return error instanceof TenderCliDbApiError ? error.nextAction : null;
12920
12660
  }
12921
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.28",
3
+ "version": "0.1.29",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "tender": "bin/tender.js"