@tenderprompt/cli 0.1.35 → 0.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +0 -4
  2. package/dist/index.js +217 -710
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -132,10 +132,6 @@ Credential precedence is explicit-first:
132
132
  4. saved default profile
133
133
 
134
134
  `--base-url` overrides the base URL for whichever credential source is used.
135
- Hosted Shopify workspace commands are the exception: `tender shopify workspace
136
- ...` runs only in Tender's hosted source terminal, requires
137
- `TENDER_BASE_URL=http://tender-api.tender.test`, and does not read
138
- `TENDER_API_TOKEN`, saved profiles, or a container-visible workspace id.
139
135
 
140
136
  ## Git Transport
141
137
 
package/dist/index.js CHANGED
@@ -199,12 +199,6 @@ Commands:
199
199
  tender connectors shopify verify
200
200
  Exchange credentials for an Admin API access token
201
201
  tender connectors bind Print an app.json binding for a connector instance
202
- tender shopify workspace connectors list
203
- List current-store Shopify connectors from a hosted workspace
204
- tender shopify workspace link-runtime --connector <connector-id>
205
- Link the hosted runtime to a Shopify connector
206
- tender shopify workspace deploy-source
207
- Deploy linked Shopify source from a hosted workspace
208
202
  tender app list List Tender App records visible to the current token
209
203
  tender app create --name <name>
210
204
  Create a new Tender App in the current account
@@ -282,9 +276,6 @@ Examples:
282
276
  printf "$SHOPIFY_APP_AUTOMATION_TOKEN" | tender connectors shopify app-automation-token set conn_123 --token-stdin --expires-at 2026-08-22 --profile account --json
283
277
  tender connectors shopify verify conn_123 --profile account --json
284
278
  tender connectors bind SHOPIFY_CONNECTOR conn_123 --instance default --json
285
- tender shopify workspace connectors list --json
286
- tender shopify workspace link-runtime --connector conn_123 --json
287
- tender shopify workspace deploy-source --json
288
279
  tender app list --json
289
280
  tender app create --name "Exit Intent Widget" --json
290
281
  tender app create --name "Data Collection App" --init --dir ./data-collection-app --preview --json
@@ -499,33 +490,6 @@ Examples:
499
490
  tender connectors shopify source deploy conn_123 --dir ./shopify-app --confirm deploy --profile account --json
500
491
  tender connectors bind SHOPIFY_CONNECTOR conn_123 --instance default --json`;
501
492
  }
502
- function shopifyWorkspaceHelp() {
503
- return `Usage: tender shopify workspace <command> [--json]
504
-
505
- Hosted Shopify workspace commands use the current terminal workspace identity.
506
- They do not read Tender profiles, account-scoped tokens, Shopify client secrets,
507
- or Shopify App Automation Tokens from the terminal.
508
-
509
- Commands:
510
- tender shopify workspace connectors list
511
- List current-store Shopify connectors available to this runtime
512
- tender shopify workspace link-runtime --connector <connector-id>
513
- Link this runtime to a Shopify connector source project
514
- tender shopify workspace validate-source
515
- Validate the linked Shopify connector source
516
- tender shopify workspace deploy-source
517
- Deploy the linked Shopify connector source
518
-
519
- Required environment:
520
- TENDER_BASE_URL
521
- TENDER_RUNTIME_ARTIFACT_ID
522
-
523
- Examples:
524
- tender shopify workspace connectors list --json
525
- tender shopify workspace link-runtime --connector conn_123 --json
526
- tender shopify workspace validate-source --json
527
- tender shopify workspace deploy-source --json`;
528
- }
529
493
  function connectorsShopifyCreateHelp() {
530
494
  return `Usage: tender connectors shopify create [--instance <id>] [--label <text>] [--app-name <text>] [--admin-api-version <version>] [--shop <myshopify-domain>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
531
495
 
@@ -611,7 +575,10 @@ function connectorsShopifySourceValidateHelp() {
611
575
  return `Usage: tender connectors shopify source validate <connector-id> [--dir <path>] [--client-id <shopify-client-id>] [--execute] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
612
576
 
613
577
  Without --execute, prints the exact Shopify CLI validation command. With
614
- the --execute flag, runs npm exec --yes --package @shopify/cli@latest -- shopify app config validate with npm lifecycle environment variables scrubbed.
578
+ the --execute flag, runs npm exec --yes --package @shopify/cli@latest -- shopify app config validate with npm lifecycle environment variables scrubbed. If
579
+ SHOPIFY_APP_AUTOMATION_TOKEN is not set locally, the CLI requests the
580
+ connector's stored App Automation Token and injects it only into the spawned
581
+ Shopify CLI child process.
615
582
 
616
583
  Examples:
617
584
  tender connectors shopify source validate conn_123 --dir ./shopify-app --execute --json`;
@@ -625,12 +592,9 @@ SHOPIFY_APP_AUTOMATION_TOKEN is not set locally, the CLI requests the
625
592
  connector's stored App Automation Token and injects it only into the spawned
626
593
  Shopify CLI child process.
627
594
 
628
- Inside hosted Shopify project workspaces, direct shopify app deploy is allowed
629
- when Shopify CLI is already authenticated there. Otherwise use
630
- tender shopify workspace deploy-source so Tender can use the connector's
631
- stored App Automation Token server-side. Do not run this account-scoped command
632
- with a hosted workspace artifact token. Never ask the merchant to paste token
633
- material into the terminal.
595
+ Inside the Shopify app, deploy connector source with the explicit **Deploy
596
+ source** action after the chat editor validates the pushed commit. Never ask the
597
+ merchant to paste token material into chat or a terminal.
634
598
 
635
599
  Examples:
636
600
  tender connectors shopify source deploy conn_123 --dir ./shopify-app --version connector-source-smoke --confirm deploy --profile account --json`;
@@ -1008,7 +972,8 @@ function analyticsChartsTemplatesHelp() {
1008
972
 
1009
973
  Print reusable chart spec templates for agents. Adapt templates to observed
1010
974
  events, paths, and property keys from analytics capabilities before querying or
1011
- saving.
975
+ saving. The hourly_dimension_breakdown_24h template documents the bounded
976
+ hour-by-property operator report.
1012
977
 
1013
978
  Examples:
1014
979
  tender app analytics charts templates --json`;
@@ -1655,7 +1620,6 @@ function tenderCliCapabilities() {
1655
1620
  'tender app outbound-auth --help',
1656
1621
  'tender connectors --help',
1657
1622
  'tender connectors shopify source init --help',
1658
- 'tender shopify workspace --help',
1659
1623
  ],
1660
1624
  },
1661
1625
  workflows: [
@@ -1700,36 +1664,6 @@ function tenderCliCapabilities() {
1700
1664
  'Fetch shopify-connector-customer-metadata-widget when the requested job involves customer metafields, metaobjects, or profile writes.',
1701
1665
  ],
1702
1666
  },
1703
- {
1704
- name: 'hosted_shopify_project_release',
1705
- when: 'Inside a hosted Shopify project workspace after editing a runtime app and linked Shopify connector source, or when the merchant asks to deploy/release/publish the Shopify connector.',
1706
- commands: [
1707
- 'tender shopify workspace connectors list --json',
1708
- 'tender shopify workspace link-runtime --connector <connector-id> --json',
1709
- 'cd /workspace/runtime-project && git push origin HEAD:main',
1710
- 'cd /workspace/runtime-project && commit=$(git rev-parse HEAD) && tender app publish "$TENDER_RUNTIME_ARTIFACT_ID" --commit "$commit" --confirm publish --stream --json',
1711
- 'cd /workspace/shopify-connector && git push origin HEAD:main',
1712
- 'cd /workspace/shopify-connector && tender shopify workspace validate-source --json',
1713
- 'cd /workspace/shopify-connector && tender shopify workspace deploy-source --json',
1714
- ],
1715
- notes: [
1716
- 'If /workspace/shopify-connector is missing, list connectors, link the selected connector, then continue in /workspace/shopify-connector unless link-runtime explicitly returns reloadRequired:true.',
1717
- 'For connector selection or switching, use tender shopify workspace connectors list --json and tender shopify workspace link-runtime --connector <connector-id> --json; do not use tender connectors list or ask for a Shopify Client ID.',
1718
- 'When selecting by merchant-provided Shopify app name, prefer an exact normalized label/appName match before any partial match; do not choose a longer connector name that merely contains the requested name.',
1719
- 'After link-runtime succeeds, tender shopify workspace validate-source and deploy-source automatically use that selected connector.',
1720
- 'For a deploy-only connector request, skip npm install, npm run check, tender app doctor, and other runtime-project checks; run cd /workspace/shopify-connector && tender shopify workspace deploy-source --json.',
1721
- 'For a validate-only connector request, run cd /workspace/shopify-connector && tender shopify workspace validate-source --json.',
1722
- 'Only run runtime-project checks when the merchant asks to change, validate, preview, or publish the runtime app.',
1723
- 'Publish the Tender runtime first and use the pinned runtime URL from publish JSON to update the linked connector source before deploying Shopify.',
1724
- 'Use TENDER_SHOPIFY_CONNECTOR_ID or connector.json for Tender connector commands; shopify.app.toml contains the Shopify client_id, not the Tender connector identity.',
1725
- 'Do not run tender connectors shopify source validate or tender connectors shopify source deploy from hosted workspaces; those commands require an account-scoped Tender CLI token.',
1726
- 'Use tender shopify workspace validate-source and deploy-source so the connector stored App Automation Token stays server-side.',
1727
- 'If Shopify CLI is already authenticated in the hosted workspace, direct shopify app config validate and shopify app deploy are allowed.',
1728
- 'Never ask for Shopify App Automation Tokens, client secrets, Tender tokens, or provider keys in the terminal.',
1729
- 'Do not release by pushing Git tags; Tender runtime publish plus tender shopify workspace deploy-source are the release path.',
1730
- 'If tender shopify workspace deploy-source reports a missing or expired App Automation Token, stop and ask the merchant to refresh the Custom App automation token in Tender.',
1731
- ],
1732
- },
1733
1667
  {
1734
1668
  name: 'create_new_app_with_preview',
1735
1669
  when: 'When the user wants a new Tender App that is immediately visible.',
@@ -2127,6 +2061,25 @@ function normalizeBaseUrl(value) {
2127
2061
  function readDefaultBaseUrl(env) {
2128
2062
  return normalizeBaseUrl(env.TENDER_BASE_URL ?? DEFAULT_BASE_URL);
2129
2063
  }
2064
+ function readDefaultShopifyApprovalContext(env) {
2065
+ const shopifyAppUrl = env.TENDER_SHOPIFY_APP_URL?.trim();
2066
+ if (shopifyAppUrl) {
2067
+ const parsed = parseShopifyAppUrl(shopifyAppUrl, 'TENDER_SHOPIFY_APP_URL');
2068
+ return {
2069
+ shopifyAppUrl: parsed.appUrl,
2070
+ shopifyStore: parsed.store,
2071
+ };
2072
+ }
2073
+ const shopifyStore = env.TENDER_SHOPIFY_STORE?.trim();
2074
+ if (shopifyStore) {
2075
+ const store = parseShopifyStoreHandle(shopifyStore, 'TENDER_SHOPIFY_STORE');
2076
+ return {
2077
+ shopifyAppUrl: shopifyAppUrlForStore(store),
2078
+ shopifyStore: store,
2079
+ };
2080
+ }
2081
+ return null;
2082
+ }
2130
2083
  function readConfigPath(env) {
2131
2084
  return env.TENDER_CONFIG ? path.resolve(env.TENDER_CONFIG) : path.join(os.homedir(), '.tender', 'config.json');
2132
2085
  }
@@ -2318,17 +2271,6 @@ async function resolveApiCredentials(input) {
2318
2271
  };
2319
2272
  }
2320
2273
  async function resolveLifecycleApiCredentials(input) {
2321
- const baseUrl = normalizeBaseUrl(input.baseUrl ?? input.env.TENDER_BASE_URL ?? DEFAULT_BASE_URL);
2322
- if (!input.flagToken &&
2323
- !input.profileName &&
2324
- isHostedWorkspaceInternalApiBaseUrl(baseUrl)) {
2325
- return {
2326
- baseUrl,
2327
- token: null,
2328
- source: 'hosted-workspace',
2329
- profileName: null,
2330
- };
2331
- }
2332
2274
  return await resolveApiCredentials(input);
2333
2275
  }
2334
2276
  function apiHeadersForCredentials(input) {
@@ -4036,59 +3978,18 @@ function parseConnectorsShopifySourceDeployArgs(args) {
4036
3978
  }
4037
3979
  throw new TenderCliUsageError(`Unknown connectors shopify source deploy option: ${arg}`);
4038
3980
  }
4039
- return { command: 'connectors shopify source deploy', connectorId, dir, clientId, version, message, sourceControlUrl, allowDeletes, confirm, ...parsed };
4040
- }
4041
- function parseShopifyWorkspaceSourceArgs(args, command) {
4042
- if (args.includes('--help') || args.includes('-h')) {
4043
- return { command: 'help', topic: command };
4044
- }
4045
- let json = false;
4046
- for (const arg of args) {
4047
- if (arg === '--json') {
4048
- json = true;
4049
- continue;
4050
- }
4051
- throw new TenderCliUsageError(`Unknown ${command} option: ${arg}`);
4052
- }
4053
- return { command, json };
4054
- }
4055
- function parseShopifyWorkspaceConnectorsListArgs(args) {
4056
- if (args.includes('--help') || args.includes('-h')) {
4057
- return { command: 'help', topic: 'shopify workspace connectors list' };
4058
- }
4059
- let json = false;
4060
- for (const arg of args) {
4061
- if (arg === '--json') {
4062
- json = true;
4063
- continue;
4064
- }
4065
- throw new TenderCliUsageError(`Unknown shopify workspace connectors list option: ${arg}`);
4066
- }
4067
- return { command: 'shopify workspace connectors list', json };
4068
- }
4069
- function parseShopifyWorkspaceLinkRuntimeArgs(args) {
4070
- if (args.includes('--help') || args.includes('-h')) {
4071
- return { command: 'help', topic: 'shopify workspace link-runtime' };
4072
- }
4073
- let connectorId = null;
4074
- let json = false;
4075
- for (let index = 0; index < args.length; index += 1) {
4076
- const arg = args[index];
4077
- if (arg === '--json') {
4078
- json = true;
4079
- continue;
4080
- }
4081
- if (arg === '--connector') {
4082
- connectorId = parseConnectorId(parseRequiredFlagValue(args[index + 1], '--connector'), 'tender shopify workspace link-runtime --connector conn_123 --json');
4083
- index += 1;
4084
- continue;
4085
- }
4086
- throw new TenderCliUsageError(`Unknown shopify workspace link-runtime option: ${arg}`);
4087
- }
4088
- if (!connectorId) {
4089
- throw new TenderCliUsageError('--connector is required. Example: tender shopify workspace link-runtime --connector conn_123 --json');
4090
- }
4091
- return { command: 'shopify workspace link-runtime', connectorId, json };
3981
+ return {
3982
+ command: 'connectors shopify source deploy',
3983
+ connectorId,
3984
+ dir,
3985
+ clientId,
3986
+ version,
3987
+ message,
3988
+ sourceControlUrl,
3989
+ allowDeletes,
3990
+ confirm,
3991
+ ...parsed,
3992
+ };
4092
3993
  }
4093
3994
  function parseConnectorsShopifyDeleteArgs(args) {
4094
3995
  if (args.includes('--help') || args.includes('-h')) {
@@ -5614,6 +5515,10 @@ function analyticsAuthoringContract() {
5614
5515
  },
5615
5516
  ],
5616
5517
  charting: {
5518
+ contractDiscovery: {
5519
+ capabilities: 'tender app analytics capabilities <app-id> --include-catalog --json',
5520
+ templates: 'tender app analytics charts templates --json',
5521
+ },
5617
5522
  starterDashboardCommands: ANALYTICS_AGENT_GUIDANCE.starterDashboardCommands,
5618
5523
  completionProof: ANALYTICS_AGENT_GUIDANCE.dashboardCompletion.proofCommand,
5619
5524
  unitFunnel: 'After traffic exists, use tender app analytics capabilities <app-id> --include-catalog --json and create version 2 unit_step_count funnel specs from observedFlows and observedUnitTypes.',
@@ -5851,6 +5756,33 @@ function analyticsChartTemplates() {
5851
5756
  limit: 10,
5852
5757
  },
5853
5758
  },
5759
+ {
5760
+ id: 'hourly_dimension_breakdown_24h',
5761
+ title: 'Hourly dimension breakdown (24h)',
5762
+ visualization: 'table',
5763
+ purpose: 'Compare one low-cardinality promoted dimension by hour over the last 24 hours.',
5764
+ usableAsIs: false,
5765
+ placeholders: {
5766
+ eventName: '<event_name>',
5767
+ property: '<queryable_dimension_property>',
5768
+ },
5769
+ adaptWith: [
5770
+ 'Replace both placeholders with one observed custom event and one active dimension property for that event.',
5771
+ 'Use only non-sensitive categorical dimensions with at most 16 observed values.',
5772
+ 'Keep the 24h range, hour-first grouping order, table visualization, custom_event_count metric, and 400-row limit unchanged.',
5773
+ ],
5774
+ spec: {
5775
+ version: 1,
5776
+ dataset: 'events',
5777
+ visualization: 'table',
5778
+ metric: { name: 'custom_event_count' },
5779
+ groupBy: [{ field: 'hour' }, { field: 'property:<queryable_dimension_property>' }],
5780
+ filters: [{ field: 'event_name', op: '=', value: '<event_name>' }],
5781
+ range: { preset: '24h' },
5782
+ sort: [{ field: 'hour', direction: 'asc' }],
5783
+ limit: 400,
5784
+ },
5785
+ },
5854
5786
  {
5855
5787
  id: 'experiment_readout',
5856
5788
  title: 'Experiment readout',
@@ -5871,10 +5803,7 @@ function analyticsChartTemplates() {
5871
5803
  dataset: 'events',
5872
5804
  visualization: 'table',
5873
5805
  metric: { name: 'custom_event_count' },
5874
- groupBy: [
5875
- { field: 'event_name' },
5876
- { field: 'property:experiment_variant' },
5877
- ],
5806
+ groupBy: [{ field: 'event_name' }, { field: 'property:experiment_variant' }],
5878
5807
  filters: [
5879
5808
  {
5880
5809
  field: 'event_name',
@@ -7185,30 +7114,6 @@ function parseTenderArgs(args) {
7185
7114
  }
7186
7115
  throw new TenderCliUsageError(`Unknown connectors command: ${args[1]}`);
7187
7116
  }
7188
- if (args[0] === 'shopify') {
7189
- if (args[1] === undefined || args[1] === '--help' || args[1] === '-h') {
7190
- return { command: 'help', topic: 'shopify' };
7191
- }
7192
- if (args[1] === 'workspace') {
7193
- if (args[2] === undefined || args[2] === '--help' || args[2] === '-h') {
7194
- return { command: 'help', topic: 'shopify workspace' };
7195
- }
7196
- if (args[2] === 'connectors' && args[3] === 'list') {
7197
- return parseShopifyWorkspaceConnectorsListArgs(args.slice(4));
7198
- }
7199
- if (args[2] === 'link-runtime') {
7200
- return parseShopifyWorkspaceLinkRuntimeArgs(args.slice(3));
7201
- }
7202
- if (args[2] === 'validate-source') {
7203
- return parseShopifyWorkspaceSourceArgs(args.slice(3), 'shopify workspace validate-source');
7204
- }
7205
- if (args[2] === 'deploy-source') {
7206
- return parseShopifyWorkspaceSourceArgs(args.slice(3), 'shopify workspace deploy-source');
7207
- }
7208
- throw new TenderCliUsageError(`Unknown shopify workspace command: ${args[2]}`);
7209
- }
7210
- throw new TenderCliUsageError(`Unknown shopify command: ${args[1]}`);
7211
- }
7212
7117
  if (args[0] === 'apps') {
7213
7118
  throw new TenderCliUsageError('Unknown command: apps. Use `tender app ...` for Tender App workflows.');
7214
7119
  }
@@ -7274,7 +7179,8 @@ function parseTenderArgs(args) {
7274
7179
  if (resourceArgs[1] === 'bindings' && (resourceArgs[2] === undefined || resourceArgs[2] === '--help' || resourceArgs[2] === '-h')) {
7275
7180
  return { command: 'help', topic: 'app bindings outbound' };
7276
7181
  }
7277
- if (resourceArgs[1] === 'outbound-auth' && (resourceArgs[2] === undefined || resourceArgs[2] === '--help' || resourceArgs[2] === '-h')) {
7182
+ if (resourceArgs[1] === 'outbound-auth' &&
7183
+ (resourceArgs[2] === undefined || resourceArgs[2] === '--help' || resourceArgs[2] === '-h')) {
7278
7184
  return { command: 'help', topic: 'app outbound-auth' };
7279
7185
  }
7280
7186
  if (resourceArgs[1] === 'outbound-auth' && resourceArgs[2] === 'list') {
@@ -7884,12 +7790,7 @@ async function requestAppDbDashboardApi(input) {
7884
7790
  (typeof payload?.reasonCode === 'string' && payload.reasonCode) ||
7885
7791
  `App database dashboard request failed with HTTP ${response.status}.`;
7886
7792
  const example = appDbDashboardCorrectedExample(input.artifactId, input.path);
7887
- throw new TenderCliDbApiError([
7888
- message,
7889
- `Scope: artifact ${input.artifactId}, app-db-dashboards.`,
7890
- `Reason: ${reasonCode}.`,
7891
- `Example: ${example}.`,
7892
- ].join('\n'), reasonCode, {
7793
+ throw new TenderCliDbApiError([message, `Scope: artifact ${input.artifactId}, app-db-dashboards.`, `Reason: ${reasonCode}.`, `Example: ${example}.`].join('\n'), reasonCode, {
7893
7794
  artifactId: input.artifactId,
7894
7795
  surface: 'db/dashboards',
7895
7796
  }, example);
@@ -7945,189 +7846,13 @@ async function requestConnectorsApi(input) {
7945
7846
  const message = (typeof payload?.message === 'string' && payload.message) ||
7946
7847
  (typeof payload?.reasonCode === 'string' && payload.reasonCode) ||
7947
7848
  `Connector request failed with HTTP ${response.status}.`;
7948
- const example = input.hostedWorkspaceHint && reasonCode === 'account_scope_required'
7949
- ? 'Hosted workspace example: tender shopify workspace connectors list --json.'
7950
- : 'Example: tender connectors list --profile account --json.';
7951
- throw new TenderCliConnectorsApiError([message, `Reason: ${reasonCode}.`, example].join('\n'), reasonCode, response.status, payload ?? null, example.replace(/^Example:\s*/i, '').replace(/^Hosted workspace example:\s*/i, '').replace(/\.$/, ''));
7849
+ const example = 'Example: tender connectors list --profile account --json.';
7850
+ throw new TenderCliConnectorsApiError([message, `Reason: ${reasonCode}.`, example].join('\n'), reasonCode, response.status, payload ?? null, example
7851
+ .replace(/^Example:\s*/i, '')
7852
+ .replace(/\.$/, ''));
7952
7853
  }
7953
7854
  return payload;
7954
7855
  }
7955
- function normalizeHostedWorkspaceBaseUrl(value) {
7956
- const baseUrl = value?.trim().replace(/\/+$/, '') ?? '';
7957
- if (!baseUrl) {
7958
- return null;
7959
- }
7960
- try {
7961
- const url = new URL(baseUrl);
7962
- return url.protocol === 'https:' || url.protocol === 'http:' ? url.toString().replace(/\/+$/, '') : null;
7963
- }
7964
- catch {
7965
- return null;
7966
- }
7967
- }
7968
- function readHostedWorkspaceArtifactId(value) {
7969
- const artifactId = value?.trim() ?? '';
7970
- return /^artifact_[A-Za-z0-9_-]+$/.test(artifactId) ? artifactId : null;
7971
- }
7972
- function isHostedWorkspaceInternalApiBaseUrl(baseUrl) {
7973
- try {
7974
- return new URL(baseUrl).hostname === 'tender-api.tender.test';
7975
- }
7976
- catch {
7977
- return false;
7978
- }
7979
- }
7980
- function hostedWorkspaceApiHeaders(input) {
7981
- return {
7982
- accept: 'application/json',
7983
- ...(input.jsonBody ? { 'content-type': 'application/json' } : {}),
7984
- };
7985
- }
7986
- function readHostedWorkspaceConnectorId(value) {
7987
- const connectorId = value?.trim() ?? '';
7988
- return /^conn_[A-Za-z0-9_-]+$/.test(connectorId) ? connectorId : null;
7989
- }
7990
- function errorCodeMatches(error, code) {
7991
- return Boolean(error &&
7992
- typeof error === 'object' &&
7993
- 'code' in error &&
7994
- error.code === code);
7995
- }
7996
- function hostedWorkspaceSelectionFilePath(runtime) {
7997
- const env = runtime.env ?? process.env;
7998
- const explicitPath = env.TENDER_WORKSPACE_STATE_FILE?.trim();
7999
- if (explicitPath) {
8000
- return path.resolve(explicitPath);
8001
- }
8002
- const runtimeProjectDir = env.TENDER_RUNTIME_PROJECT_DIR?.trim();
8003
- if (runtimeProjectDir && path.isAbsolute(runtimeProjectDir)) {
8004
- return path.join(path.dirname(runtimeProjectDir), '.tender', 'shopify-workspace.json');
8005
- }
8006
- const cwd = process.cwd();
8007
- if (cwd === '/workspace' || cwd.startsWith('/workspace/')) {
8008
- return '/workspace/.tender/shopify-workspace.json';
8009
- }
8010
- return null;
8011
- }
8012
- async function writeHostedWorkspaceConnectorSelection(input, runtime) {
8013
- const filePath = hostedWorkspaceSelectionFilePath(runtime);
8014
- if (!filePath) {
8015
- return;
8016
- }
8017
- await mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
8018
- await writeFile(filePath, `${JSON.stringify({
8019
- version: 1,
8020
- runtimeArtifactId: input.runtimeArtifactId,
8021
- connectorId: input.connectorId,
8022
- selectedAt: Date.now(),
8023
- }, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
8024
- }
8025
- async function readHostedWorkspaceConnectorSelection(runtimeArtifactId, runtime) {
8026
- const env = runtime.env ?? process.env;
8027
- const envConnectorId = readHostedWorkspaceConnectorId(env.TENDER_SHOPIFY_CONNECTOR_ID);
8028
- if (envConnectorId) {
8029
- return envConnectorId;
8030
- }
8031
- const filePath = hostedWorkspaceSelectionFilePath(runtime);
8032
- if (!filePath) {
8033
- return null;
8034
- }
8035
- let content;
8036
- try {
8037
- content = await readFile(filePath, 'utf8');
8038
- }
8039
- catch (error) {
8040
- if (errorCodeMatches(error, 'ENOENT')) {
8041
- return null;
8042
- }
8043
- throw error;
8044
- }
8045
- let parsed;
8046
- try {
8047
- parsed = JSON.parse(content);
8048
- }
8049
- catch {
8050
- return null;
8051
- }
8052
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
8053
- return null;
8054
- }
8055
- const record = parsed;
8056
- if (record.runtimeArtifactId !== runtimeArtifactId) {
8057
- return null;
8058
- }
8059
- return readHostedWorkspaceConnectorId(typeof record.connectorId === 'string' ? record.connectorId : undefined);
8060
- }
8061
- function shopifyWorkspaceCommandApiPath(command) {
8062
- return command === 'shopify workspace deploy-source' ? 'deploy' : 'validate';
8063
- }
8064
- async function requestShopifyWorkspaceSourceCommand(input) {
8065
- const url = `${input.baseUrl}/api/account/connectors/${encodeURIComponent(input.connectorId)}/source/${shopifyWorkspaceCommandApiPath(input.command)}`;
8066
- const response = await input.fetcher(url, {
8067
- method: 'POST',
8068
- headers: hostedWorkspaceApiHeaders({
8069
- jsonBody: true,
8070
- }),
8071
- body: JSON.stringify({
8072
- sourceArtifactId: input.sourceArtifactId,
8073
- runtimeArtifactId: input.runtimeArtifactId,
8074
- }),
8075
- });
8076
- const payload = response.status === 204 ? { ok: true } : await parseJsonResponse(response);
8077
- if (payload) {
8078
- return { status: response.status, payload };
8079
- }
8080
- return {
8081
- status: response.status,
8082
- payload: {
8083
- ok: false,
8084
- reasonCode: `http_${response.status}`,
8085
- message: `Shopify workspace request failed with HTTP ${response.status}.`,
8086
- },
8087
- };
8088
- }
8089
- async function requestShopifyWorkspaceConnectorsList(input) {
8090
- const params = new URLSearchParams({ runtimeArtifactId: input.runtimeArtifactId });
8091
- const url = `${input.baseUrl}/api/account/connectors?${params.toString()}`;
8092
- const response = await input.fetcher(url, {
8093
- method: 'GET',
8094
- headers: hostedWorkspaceApiHeaders({}),
8095
- });
8096
- const payload = response.status === 204 ? { ok: true } : await parseJsonResponse(response);
8097
- if (payload) {
8098
- return { status: response.status, payload };
8099
- }
8100
- return {
8101
- status: response.status,
8102
- payload: {
8103
- ok: false,
8104
- reasonCode: `http_${response.status}`,
8105
- message: `Shopify workspace connectors request failed with HTTP ${response.status}.`,
8106
- },
8107
- };
8108
- }
8109
- async function requestShopifyWorkspaceLinkRuntime(input) {
8110
- const url = `${input.baseUrl}/api/account/connectors/${encodeURIComponent(input.connectorId)}/runtime-links`;
8111
- const response = await input.fetcher(url, {
8112
- method: 'POST',
8113
- headers: hostedWorkspaceApiHeaders({
8114
- jsonBody: true,
8115
- }),
8116
- body: JSON.stringify({ runtimeArtifactId: input.runtimeArtifactId }),
8117
- });
8118
- const payload = response.status === 204 ? { ok: true } : await parseJsonResponse(response);
8119
- if (payload) {
8120
- return { status: response.status, payload };
8121
- }
8122
- return {
8123
- status: response.status,
8124
- payload: {
8125
- ok: false,
8126
- reasonCode: `http_${response.status}`,
8127
- message: `Shopify workspace link request failed with HTTP ${response.status}.`,
8128
- },
8129
- };
8130
- }
8131
7856
  function dbCorrectedExample(artifactId, dbPath) {
8132
7857
  if (dbPath === 'capabilities') {
8133
7858
  return `tender app db capabilities ${artifactId} --json`;
@@ -8242,26 +7967,29 @@ function readAnalyticsSpecMetricName(spec) {
8242
7967
  }
8243
7968
  return readCliStringField(spec.metric, 'name');
8244
7969
  }
8245
- function readAnalyticsSpecGroupField(spec) {
7970
+ function readAnalyticsSpecGroupFields(spec) {
8246
7971
  if (!isCliJsonRecord(spec) || !Array.isArray(spec.groupBy)) {
8247
- return null;
7972
+ return [];
8248
7973
  }
8249
- const [group] = spec.groupBy.filter(isCliJsonRecord);
8250
- return group ? readCliStringField(group, 'field') : null;
7974
+ return spec.groupBy
7975
+ .filter(isCliJsonRecord)
7976
+ .map((group) => readCliStringField(group, 'field'))
7977
+ .filter((field) => field !== null);
8251
7978
  }
8252
7979
  function readRequiredAnalyticsPropertiesFromSpec(spec) {
8253
7980
  const eventName = readAnalyticsSpecEventNameFilter(spec);
8254
7981
  const requirements = [];
8255
- const groupField = readAnalyticsSpecGroupField(spec);
8256
- if (groupField?.startsWith('property:')) {
8257
- if (!eventName) {
8258
- throw new TenderCliUsageError('Property group-by chart specs require an event_name = filter so the CLI can make the property available to dashboard queries.');
7982
+ for (const groupField of readAnalyticsSpecGroupFields(spec)) {
7983
+ if (groupField.startsWith('property:')) {
7984
+ if (!eventName) {
7985
+ throw new TenderCliUsageError('Property group-by chart specs require an event_name = filter so the CLI can make the property available to dashboard queries.');
7986
+ }
7987
+ requirements.push({
7988
+ eventName,
7989
+ propertyKey: groupField.slice('property:'.length),
7990
+ propertyType: 'dimension',
7991
+ });
8259
7992
  }
8260
- requirements.push({
8261
- eventName,
8262
- propertyKey: groupField.slice('property:'.length),
8263
- propertyType: 'dimension',
8264
- });
8265
7993
  }
8266
7994
  const metricName = readAnalyticsSpecMetricName(spec);
8267
7995
  if (metricName?.startsWith('sum:') || metricName?.startsWith('avg:')) {
@@ -8432,7 +8160,16 @@ function csvCell(value) {
8432
8160
  return text;
8433
8161
  }
8434
8162
  function rowsToCsv(rows) {
8435
- const records = rows.filter((row) => Boolean(row) && typeof row === 'object' && !Array.isArray(row));
8163
+ const records = rows
8164
+ .filter((row) => Boolean(row) && typeof row === 'object' && !Array.isArray(row))
8165
+ .map((row) => {
8166
+ const dimensions = row.dimensions;
8167
+ if (!dimensions || typeof dimensions !== 'object' || Array.isArray(dimensions)) {
8168
+ return row;
8169
+ }
8170
+ const { dimensions: _dimensions, ...rest } = row;
8171
+ return { ...dimensions, ...rest };
8172
+ });
8436
8173
  const headers = Array.from(records.reduce((set, row) => {
8437
8174
  for (const key of Object.keys(row)) {
8438
8175
  set.add(key);
@@ -9058,11 +8795,13 @@ async function runAuthLogin(command, io, runtime) {
9058
8795
  const fetcher = runtime.fetcher ?? fetch;
9059
8796
  const sleeper = runtime.sleeper ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
9060
8797
  const now = runtime.now ?? Date.now;
8798
+ const agentAutoPoll = command.json && env.TENDER_AGENT_DEVICE_AUTH_AUTO_POLL === '1';
9061
8799
  const config = await readConfig(env);
9062
8800
  const existingProfile = config.profiles?.[command.saveProfile] ?? null;
9063
8801
  const baseUrl = command.baseUrl ?? existingProfile?.baseUrl ?? readDefaultBaseUrl(env);
9064
- const shopifyAppUrl = command.shopifyAppUrl ?? existingProfile?.shopifyAppUrl ?? null;
9065
- const shopifyStore = command.shopifyStore ?? existingProfile?.shopifyStore ?? null;
8802
+ const defaultShopifyApprovalContext = readDefaultShopifyApprovalContext(env);
8803
+ const shopifyAppUrl = command.shopifyAppUrl ?? existingProfile?.shopifyAppUrl ?? defaultShopifyApprovalContext?.shopifyAppUrl ?? null;
8804
+ const shopifyStore = command.shopifyStore ?? existingProfile?.shopifyStore ?? defaultShopifyApprovalContext?.shopifyStore ?? null;
9066
8805
  const deviceResponse = await fetcher(`${baseUrl}/oauth/device/code`, {
9067
8806
  method: 'POST',
9068
8807
  headers: {
@@ -9135,7 +8874,7 @@ async function runAuthLogin(command, io, runtime) {
9135
8874
  configPath: readConfigPath(env),
9136
8875
  nextCommand: buildAuthStatusNextCommand(command.saveProfile),
9137
8876
  };
9138
- if (command.noPoll) {
8877
+ if (command.noPoll && !agentAutoPoll) {
9139
8878
  if (command.json) {
9140
8879
  io.stdout(JSON.stringify(pendingResult, null, 2));
9141
8880
  }
@@ -9148,10 +8887,7 @@ async function runAuthLogin(command, io, runtime) {
9148
8887
  io.stderr(JSON.stringify(pendingResult, null, 2));
9149
8888
  }
9150
8889
  else {
9151
- io.stdout([
9152
- `Open this URL to approve Tender CLI access: ${verificationUriComplete}`,
9153
- `User code: ${devicePayload.user_code}`,
9154
- ].join('\n'));
8890
+ io.stdout([`Open this URL to approve Tender CLI access: ${verificationUriComplete}`, `User code: ${devicePayload.user_code}`].join('\n'));
9155
8891
  }
9156
8892
  let intervalSeconds = Math.max(1, devicePayload.interval ?? 5);
9157
8893
  let attemptsRemaining = Math.max(1, Math.ceil(command.pollTimeoutSeconds / intervalSeconds));
@@ -9848,12 +9584,7 @@ async function resolveGitCommitArgument(input) {
9848
9584
  return commitSha.toLowerCase();
9849
9585
  }
9850
9586
  const detail = result.stderr.trim() || result.stdout.trim();
9851
- throw new TenderCliUsageError([
9852
- `--commit must be a 40-character commit SHA or a Git ref resolvable in this checkout: ${value}`,
9853
- detail,
9854
- ]
9855
- .filter(Boolean)
9856
- .join('\n'));
9587
+ throw new TenderCliUsageError([`--commit must be a 40-character commit SHA or a Git ref resolvable in this checkout: ${value}`, detail].filter(Boolean).join('\n'));
9857
9588
  }
9858
9589
  function readCommandCommitSha(command) {
9859
9590
  const commitSha = 'commitSha' in command ? command.commitSha : null;
@@ -10824,9 +10555,7 @@ async function postLifecycle(input) {
10824
10555
  },
10825
10556
  runtime: input.runtime,
10826
10557
  });
10827
- const memorySync = input.operation === 'publish'
10828
- ? await flushAutomaticMemory({ repoPath, remoteName: 'tender', runtime: input.runtime })
10829
- : null;
10558
+ const memorySync = input.operation === 'publish' ? await flushAutomaticMemory({ repoPath, remoteName: 'tender', runtime: input.runtime }) : null;
10830
10559
  const resultWithMemory = {
10831
10560
  ...result,
10832
10561
  memory,
@@ -11153,7 +10882,7 @@ async function runOutboundAuthApprove(command, io, runtime) {
11153
10882
  });
11154
10883
  const fetcher = runtime.fetcher ?? fetch;
11155
10884
  const grants = command.approveAll
11156
- ? (await requestOutboundAuthList({ command, credentials, fetcher })).grants?.filter((grant) => !grant.approved) ?? []
10885
+ ? ((await requestOutboundAuthList({ command, credentials, fetcher })).grants?.filter((grant) => !grant.approved) ?? [])
11157
10886
  : [readExplicitOutboundAuthGrant(command)];
11158
10887
  const approved = [];
11159
10888
  for (const grant of grants) {
@@ -11173,11 +10902,7 @@ async function runOutboundAuthApprove(command, io, runtime) {
11173
10902
  io.stdout(JSON.stringify(result, null, 2));
11174
10903
  }
11175
10904
  else {
11176
- io.stdout([
11177
- `app_id: ${command.artifactId}`,
11178
- `approved: ${approved.length}`,
11179
- ...approved.map((grant) => formatOutboundAuthGrant(grant)),
11180
- ].join('\n'));
10905
+ io.stdout([`app_id: ${command.artifactId}`, `approved: ${approved.length}`, ...approved.map((grant) => formatOutboundAuthGrant(grant))].join('\n'));
11181
10906
  }
11182
10907
  return 0;
11183
10908
  }
@@ -11596,7 +11321,9 @@ async function runMemoryBrief(command, io, runtime) {
11596
11321
  const unresolved = entries
11597
11322
  .filter((entry) => entry.status === 'open' || entry.kind === 'failure' || entry.kind === 'open_question' || entry.kind === 'support_handoff')
11598
11323
  .slice(-command.limit);
11599
- const briefEntries = latestSummary ? [latestSummary, ...unresolved.filter((entry) => entry.entryId !== latestSummary.entryId)] : unresolved;
11324
+ const briefEntries = latestSummary
11325
+ ? [latestSummary, ...unresolved.filter((entry) => entry.entryId !== latestSummary.entryId)]
11326
+ : unresolved;
11600
11327
  const result = {
11601
11328
  ok: true,
11602
11329
  command: command.command,
@@ -11632,11 +11359,7 @@ async function runMemoryFlush(command, io, runtime) {
11632
11359
  io.stdout(JSON.stringify(result, null, 2));
11633
11360
  }
11634
11361
  else {
11635
- io.stdout([
11636
- `memory_sync: ${sync.status}`,
11637
- sync.error ? `error: ${sync.error}` : null,
11638
- result.next ? `next: ${result.next}` : null,
11639
- ]
11362
+ io.stdout([`memory_sync: ${sync.status}`, sync.error ? `error: ${sync.error}` : null, result.next ? `next: ${result.next}` : null]
11640
11363
  .filter((line) => Boolean(line))
11641
11364
  .join('\n'));
11642
11365
  }
@@ -11854,7 +11577,6 @@ async function runConnectorsList(command, io, runtime) {
11854
11577
  baseUrl: credentials.baseUrl,
11855
11578
  token: credentials.token,
11856
11579
  fetcher: runtime.fetcher ?? fetch,
11857
- hostedWorkspaceHint: Boolean((runtime.env ?? process.env).TENDER_RUNTIME_ARTIFACT_ID),
11858
11580
  });
11859
11581
  const connectors = readConnectorArray(payload);
11860
11582
  printConnectorsPayload(command, io, payload, connectors.length > 0
@@ -11907,10 +11629,7 @@ async function runConnectorsShopifySetupUrl(command, io, runtime) {
11907
11629
  token: credentials.token,
11908
11630
  fetcher: runtime.fetcher ?? fetch,
11909
11631
  });
11910
- printConnectorsPayload(command, io, payload, [
11911
- `connector_id: ${command.connectorId}`,
11912
- `setup_url: ${String(payload.setupUrl ?? '')}`,
11913
- ]);
11632
+ printConnectorsPayload(command, io, payload, [`connector_id: ${command.connectorId}`, `setup_url: ${String(payload.setupUrl ?? '')}`]);
11914
11633
  return 0;
11915
11634
  }
11916
11635
  function readClientSecret(command, runtime) {
@@ -12147,7 +11866,10 @@ merchant-editable settings.
12147
11866
  `;
12148
11867
  return new Map([
12149
11868
  ['extensions/tender-product-survey/README.md', readme],
12150
- ['extensions/tender-product-survey/package.json', '{\n "name": "tender-product-survey",\n "private": true,\n "version": "0.0.0"\n}\n'],
11869
+ [
11870
+ 'extensions/tender-product-survey/package.json',
11871
+ '{\n "name": "tender-product-survey",\n "private": true,\n "version": "0.0.0"\n}\n',
11872
+ ],
12151
11873
  ['extensions/tender-product-survey/shopify.extension.toml', extensionToml],
12152
11874
  ['extensions/tender-product-survey/blocks/product_survey.liquid', blockLiquid],
12153
11875
  ['extensions/tender-product-survey/locales/en.default.json', '{}\n'],
@@ -12248,16 +11970,10 @@ npm exec --yes --package @shopify/cli@latest -- shopify app config validate --pa
12248
11970
  tender connectors shopify source deploy ${input.connectorId} --dir . --confirm deploy --profile account --json
12249
11971
  \`\`\`
12250
11972
 
12251
- Inside the hosted Shopify workspace, commit and push source changes, then
12252
- validate or deploy from the embedded Shopify app's Deploy source action so
12253
- Tender can use the connector's stored App Automation Token server-side. If
12254
- Shopify CLI is already authenticated in the hosted workspace, direct
12255
- \`shopify app config validate\` and \`shopify app deploy\` are allowed. Do not run
12256
- \`tender connectors shopify source validate\` or
12257
- \`tender connectors shopify source deploy\` from hosted workspaces; those
12258
- commands require an account-scoped Tender CLI token. Never ask for Shopify App
12259
- Automation Tokens, client secrets, Tender tokens, or provider keys in the
12260
- terminal.
11973
+ Inside Shopify, request source changes in the Custom app editor. Tender
11974
+ validates the pushed commit, and the explicit Deploy source action uses the
11975
+ connector's stored App Automation Token server-side. Never ask for Shopify App
11976
+ Automation Tokens, client secrets, Tender tokens, or provider keys in chat.
12261
11977
  `;
12262
11978
  const files = new Map([
12263
11979
  ['.gitignore', '.shopify/\nnode_modules/\n.env\n.env.*\n'],
@@ -12500,15 +12216,7 @@ async function commitAndPushSourceFiles(input) {
12500
12216
  };
12501
12217
  }
12502
12218
  function shopifyValidateArgs(input) {
12503
- return [
12504
- 'app',
12505
- 'config',
12506
- 'validate',
12507
- '--path',
12508
- input.dir,
12509
- '--json',
12510
- ...(input.clientId ? ['--client-id', input.clientId] : []),
12511
- ];
12219
+ return ['app', 'config', 'validate', '--path', input.dir, '--json', ...(input.clientId ? ['--client-id', input.clientId] : [])];
12512
12220
  }
12513
12221
  function shopifyDeployArgs(input) {
12514
12222
  return [
@@ -12636,18 +12344,18 @@ function childShopifyDeployEnv(_runtime, token) {
12636
12344
  function connectorsProfileFlag(command) {
12637
12345
  return command.profileName ? `--profile ${command.profileName}` : '--profile account';
12638
12346
  }
12639
- function shopifyDeployTokenRecoveryMessage(command, reasonCode) {
12347
+ function shopifyCliTokenRecoveryMessage(command, reasonCode) {
12640
12348
  const profileFlag = connectorsProfileFlag(command);
12641
12349
  return [
12642
12350
  reasonCode === 'shopify_deploy_token_expired'
12643
12351
  ? 'The connector stored Shopify App Automation Token is expired.'
12644
- : 'The connector does not have a stored Shopify App Automation Token for source deploys.',
12352
+ : 'The connector does not have a stored Shopify App Automation Token for source validation and deployment.',
12645
12353
  `Store a fresh token: printf "$SHOPIFY_APP_AUTOMATION_TOKEN" | tender connectors shopify app-automation-token set ${command.connectorId} --token-stdin ${profileFlag} --json`,
12646
12354
  `Setup URL: tender connectors shopify setup-url ${command.connectorId} ${profileFlag} --json`,
12647
- 'Fallback: export SHOPIFY_APP_AUTOMATION_TOKEN locally before running source deploy.',
12355
+ 'Fallback: export SHOPIFY_APP_AUTOMATION_TOKEN locally before running source validation or deployment.',
12648
12356
  ].join('\n');
12649
12357
  }
12650
- async function resolveShopifyDeployTokenEnvironment(input) {
12358
+ async function resolveShopifyCliTokenEnvironment(input) {
12651
12359
  const localToken = localShopifyDeployToken(input.runtime);
12652
12360
  if (localToken) {
12653
12361
  return {
@@ -12660,7 +12368,9 @@ async function resolveShopifyDeployTokenEnvironment(input) {
12660
12368
  const handoff = await fetchShopifyDeployToken({
12661
12369
  connectorId: input.command.connectorId,
12662
12370
  sourceArtifactId: input.sourceArtifactId,
12663
- version: input.command.version,
12371
+ version: input.command.command === 'connectors shopify source deploy'
12372
+ ? input.command.version
12373
+ : null,
12664
12374
  credentials: input.credentials,
12665
12375
  fetcher: input.fetcher,
12666
12376
  });
@@ -12673,7 +12383,7 @@ async function resolveShopifyDeployTokenEnvironment(input) {
12673
12383
  catch (error) {
12674
12384
  if (error instanceof TenderCliConnectorsApiError &&
12675
12385
  (error.reasonCode === 'shopify_deploy_token_required' || error.reasonCode === 'shopify_deploy_token_expired')) {
12676
- const recovery = shopifyDeployTokenRecoveryMessage(input.command, error.reasonCode);
12386
+ const recovery = shopifyCliTokenRecoveryMessage(input.command, error.reasonCode);
12677
12387
  throw new TenderCliGuidedUsageError(recovery, recovery);
12678
12388
  }
12679
12389
  throw error;
@@ -12739,7 +12449,14 @@ async function runConnectorsShopifySourceInit(command, io, runtime) {
12739
12449
  commit: gitCommit,
12740
12450
  },
12741
12451
  validateCommand: shopifyCliCommandLine(shopifyValidateArgs({ dir, clientId: command.clientId })),
12742
- deployCommand: shopifyCliCommandLine(shopifyDeployArgs({ dir, clientId: command.clientId, version: 'connector-source', message: null, sourceControlUrl: null, allowDeletes: false })),
12452
+ deployCommand: shopifyCliCommandLine(shopifyDeployArgs({
12453
+ dir,
12454
+ clientId: command.clientId,
12455
+ version: 'connector-source',
12456
+ message: null,
12457
+ sourceControlUrl: null,
12458
+ allowDeletes: false,
12459
+ })),
12743
12460
  };
12744
12461
  printConnectorsPayload(command, io, result, [
12745
12462
  `connector_id: ${command.connectorId}`,
@@ -12787,7 +12504,7 @@ async function runConnectorsShopifySourceCheckout(command, io, runtime) {
12787
12504
  return 0;
12788
12505
  }
12789
12506
  async function runConnectorsShopifySourceValidate(command, io, runtime) {
12790
- const { connector } = await requestConnector(command, runtime);
12507
+ const { credentials, connector } = await requestConnector(command, runtime);
12791
12508
  const sourceArtifactId = readConnectorSourceArtifactId(connector);
12792
12509
  if (!sourceArtifactId) {
12793
12510
  throw new TenderCliUsageError('Connector does not have a source artifact yet. Run tender connectors shopify source init <connector-id> --dir ./shopify-app first.');
@@ -12801,11 +12518,46 @@ async function runConnectorsShopifySourceValidate(command, io, runtime) {
12801
12518
  }
12802
12519
  }
12803
12520
  if (!command.execute) {
12804
- printConnectorsPayload(command, io, { ok: true, dryRun: true, command: command.command, connectorId: command.connectorId, sourceArtifactId, dir, shopifyCommand: shopifyCliCommandLine(args) }, [`connector_id: ${command.connectorId}`, `source_artifact_id: ${sourceArtifactId}`, `source_dir: ${dir}`, `shopify_command: ${shopifyCliCommandLine(args)}`]);
12521
+ printConnectorsPayload(command, io, {
12522
+ ok: true,
12523
+ dryRun: true,
12524
+ command: command.command,
12525
+ connectorId: command.connectorId,
12526
+ sourceArtifactId,
12527
+ dir,
12528
+ shopifyCommand: shopifyCliCommandLine(args),
12529
+ }, [
12530
+ `connector_id: ${command.connectorId}`,
12531
+ `source_artifact_id: ${sourceArtifactId}`,
12532
+ `source_dir: ${dir}`,
12533
+ `shopify_command: ${shopifyCliCommandLine(args)}`,
12534
+ ]);
12805
12535
  return 0;
12806
12536
  }
12807
- const result = await runShopifyCliCommand({ args, cwd: dir, runtime });
12808
- printConnectorsPayload(command, io, { ok: true, command: command.command, connectorId: command.connectorId, sourceArtifactId, dir, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr }, [`connector_id: ${command.connectorId}`, `source_artifact_id: ${sourceArtifactId}`, `source_dir: ${dir}`, 'validation: passed']);
12537
+ const validationToken = await resolveShopifyCliTokenEnvironment({
12538
+ command,
12539
+ sourceArtifactId,
12540
+ credentials,
12541
+ runtime,
12542
+ fetcher: runtime.fetcher ?? fetch,
12543
+ });
12544
+ const result = await runShopifyCliCommand({
12545
+ args,
12546
+ cwd: dir,
12547
+ runtime,
12548
+ env: validationToken.env,
12549
+ redactions: validationToken.redactions,
12550
+ });
12551
+ printConnectorsPayload(command, io, {
12552
+ ok: true,
12553
+ command: command.command,
12554
+ connectorId: command.connectorId,
12555
+ sourceArtifactId,
12556
+ dir,
12557
+ exitCode: result.exitCode,
12558
+ stdout: result.stdout,
12559
+ stderr: result.stderr,
12560
+ }, [`connector_id: ${command.connectorId}`, `source_artifact_id: ${sourceArtifactId}`, `source_dir: ${dir}`, 'validation: passed']);
12809
12561
  return 0;
12810
12562
  }
12811
12563
  async function runConnectorsShopifySourceDeploy(command, io, runtime) {
@@ -12825,10 +12577,25 @@ async function runConnectorsShopifySourceDeploy(command, io, runtime) {
12825
12577
  allowDeletes: command.allowDeletes,
12826
12578
  });
12827
12579
  if (command.confirm !== 'deploy') {
12828
- printConnectorsPayload(command, io, { ok: true, dryRun: true, command: command.command, connectorId: command.connectorId, sourceArtifactId, dir, shopifyCommand: shopifyCliCommandLine(args), confirmRequired: 'deploy' }, [`connector_id: ${command.connectorId}`, `source_artifact_id: ${sourceArtifactId}`, `source_dir: ${dir}`, `shopify_command: ${shopifyCliCommandLine(args)}`, 'confirm_required: --confirm deploy']);
12580
+ printConnectorsPayload(command, io, {
12581
+ ok: true,
12582
+ dryRun: true,
12583
+ command: command.command,
12584
+ connectorId: command.connectorId,
12585
+ sourceArtifactId,
12586
+ dir,
12587
+ shopifyCommand: shopifyCliCommandLine(args),
12588
+ confirmRequired: 'deploy',
12589
+ }, [
12590
+ `connector_id: ${command.connectorId}`,
12591
+ `source_artifact_id: ${sourceArtifactId}`,
12592
+ `source_dir: ${dir}`,
12593
+ `shopify_command: ${shopifyCliCommandLine(args)}`,
12594
+ 'confirm_required: --confirm deploy',
12595
+ ]);
12829
12596
  return 0;
12830
12597
  }
12831
- const deployToken = await resolveShopifyDeployTokenEnvironment({
12598
+ const deployToken = await resolveShopifyCliTokenEnvironment({
12832
12599
  command,
12833
12600
  sourceArtifactId,
12834
12601
  credentials,
@@ -12836,266 +12603,17 @@ async function runConnectorsShopifySourceDeploy(command, io, runtime) {
12836
12603
  fetcher,
12837
12604
  });
12838
12605
  const result = await runShopifyCliCommand({ args, cwd: dir, runtime, env: deployToken.env, redactions: deployToken.redactions });
12839
- printConnectorsPayload(command, io, { ok: true, command: command.command, connectorId: command.connectorId, sourceArtifactId, dir, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr }, [`connector_id: ${command.connectorId}`, `source_artifact_id: ${sourceArtifactId}`, `source_dir: ${dir}`, 'deploy: completed']);
12840
- return 0;
12841
- }
12842
- function shopifyWorkspaceResultKey(command) {
12843
- return command === 'shopify workspace deploy-source' ? 'deployment' : 'validation';
12844
- }
12845
- function shopifyWorkspaceCommandSucceeded(command, payload) {
12846
- if (payload.ok !== true) {
12847
- return false;
12848
- }
12849
- const result = payload[shopifyWorkspaceResultKey(command.command)];
12850
- if (result && typeof result === 'object' && !Array.isArray(result)) {
12851
- const ok = result.ok;
12852
- return ok !== false;
12853
- }
12854
- return true;
12855
- }
12856
- function shopifyWorkspaceTextLines(command, payload) {
12857
- const connector = payload.connector && typeof payload.connector === 'object' && !Array.isArray(payload.connector)
12858
- ? payload.connector
12859
- : {};
12860
- const source = payload.source && typeof payload.source === 'object' && !Array.isArray(payload.source)
12861
- ? payload.source
12862
- : {};
12863
- const result = payload[shopifyWorkspaceResultKey(command.command)];
12864
- const resultRecord = result && typeof result === 'object' && !Array.isArray(result)
12865
- ? result
12866
- : {};
12867
- const succeeded = shopifyWorkspaceCommandSucceeded(command, payload);
12868
- const verb = command.command === 'shopify workspace deploy-source' ? 'deploy' : 'validate';
12869
- return [
12870
- `${verb}: ${succeeded ? 'completed' : 'failed'}`,
12871
- `connector_id: ${String(connector.connectorId ?? '')}`,
12872
- `source_artifact_id: ${String(source.sourceArtifactId ?? '')}`,
12873
- ...(payload.reasonCode ? [`reason: ${String(payload.reasonCode)}`] : []),
12874
- ...(resultRecord.exitCode !== undefined ? [`exit_code: ${String(resultRecord.exitCode)}`] : []),
12875
- ...(resultRecord.stderr ? [`stderr: ${String(resultRecord.stderr).trim()}`] : []),
12876
- ].filter((line) => !line.endsWith(': '));
12877
- }
12878
- function readShopifyWorkspaceRuntimeIdentity(runtime) {
12879
- const env = runtime.env ?? process.env;
12880
- const baseUrl = normalizeHostedWorkspaceBaseUrl(env.TENDER_BASE_URL);
12881
- if (!baseUrl) {
12882
- throw new TenderCliUsageError('TENDER_BASE_URL is required. Open or reload the hosted Shopify workspace and run this command inside its terminal.');
12883
- }
12884
- if (!isHostedWorkspaceInternalApiBaseUrl(baseUrl)) {
12885
- throw new TenderCliUsageError('TENDER_BASE_URL must be http://tender-api.tender.test inside a hosted Shopify workspace. Reload the workspace terminal and try again.');
12886
- }
12887
- const runtimeArtifactId = readHostedWorkspaceArtifactId(env.TENDER_RUNTIME_ARTIFACT_ID);
12888
- if (!runtimeArtifactId) {
12889
- throw new TenderCliUsageError('TENDER_RUNTIME_ARTIFACT_ID is required. This command only runs inside a hosted Shopify project workspace.');
12890
- }
12891
- return { baseUrl, runtimeArtifactId };
12892
- }
12893
- function readShopifyWorkspaceConnectorRecords(payload) {
12894
- return Array.isArray(payload.connectors)
12895
- ? payload.connectors.filter((connector) => Boolean(connector) && typeof connector === 'object' && !Array.isArray(connector))
12896
- : [];
12897
- }
12898
- function readShopifyWorkspaceConnectorSourceArtifactId(connector) {
12899
- const sourceProject = connector.sourceProject && typeof connector.sourceProject === 'object' && !Array.isArray(connector.sourceProject)
12900
- ? connector.sourceProject
12901
- : null;
12902
- return readHostedWorkspaceArtifactId(typeof sourceProject?.sourceArtifactId === 'string' ? sourceProject.sourceArtifactId : undefined);
12903
- }
12904
- function resolveShopifyWorkspaceSourceCommandConnector(input) {
12905
- const connectors = readShopifyWorkspaceConnectorRecords(input.payload);
12906
- const connector = input.selectedConnectorId
12907
- ? connectors.find((candidate) => candidate.connectorId === input.selectedConnectorId) ?? null
12908
- : (() => {
12909
- const linked = connectors.filter((candidate) => candidate.linked === true);
12910
- return linked.length === 1 ? linked[0] ?? null : null;
12911
- })();
12912
- if (!connector) {
12913
- return {
12914
- ok: false,
12915
- payload: {
12916
- ok: false,
12917
- reasonCode: input.selectedConnectorId
12918
- ? 'shopify_workspace_selected_connector_not_found'
12919
- : 'shopify_workspace_connector_selection_required',
12920
- message: input.selectedConnectorId
12921
- ? 'The selected Shopify connector is no longer available to this workspace.'
12922
- : 'Link this runtime to one Shopify connector before validating or deploying source.',
12923
- },
12924
- };
12925
- }
12926
- const connectorId = typeof connector.connectorId === 'string' ? connector.connectorId : null;
12927
- const sourceArtifactId = readShopifyWorkspaceConnectorSourceArtifactId(connector);
12928
- if (!connectorId || !sourceArtifactId) {
12929
- return {
12930
- ok: false,
12931
- payload: {
12932
- ok: false,
12933
- reasonCode: 'shopify_source_project_required',
12934
- message: 'The selected Shopify connector does not have a source project.',
12935
- },
12936
- };
12937
- }
12938
- return {
12606
+ printConnectorsPayload(command, io, {
12939
12607
  ok: true,
12940
- connectorId,
12941
- sourceArtifactId,
12942
- };
12943
- }
12944
- async function runShopifyWorkspaceSourceCommand(command, io, runtime) {
12945
- const { baseUrl, runtimeArtifactId } = readShopifyWorkspaceRuntimeIdentity(runtime);
12946
- const selectedConnectorId = await readHostedWorkspaceConnectorSelection(runtimeArtifactId, runtime);
12947
- const connectorsResult = await requestShopifyWorkspaceConnectorsList({
12948
- baseUrl,
12949
- runtimeArtifactId,
12950
- fetcher: runtime.fetcher ?? fetch,
12951
- });
12952
- if (!shopifyWorkspaceRequestSucceeded(connectorsResult)) {
12953
- const payload = {
12954
- ...connectorsResult.payload,
12955
- command: command.command,
12956
- runtimeArtifactId,
12957
- ...(selectedConnectorId ? { selectedConnectorId } : {}),
12958
- };
12959
- if (command.json) {
12960
- io.stdout(JSON.stringify(payload, null, 2));
12961
- }
12962
- else {
12963
- io.stdout(shopifyWorkspaceTextLines(command, payload).join('\n'));
12964
- }
12965
- return 1;
12966
- }
12967
- const resolvedConnector = resolveShopifyWorkspaceSourceCommandConnector({
12968
- payload: connectorsResult.payload,
12969
- selectedConnectorId,
12970
- });
12971
- if (!resolvedConnector.ok) {
12972
- const payload = {
12973
- ...resolvedConnector.payload,
12974
- command: command.command,
12975
- runtimeArtifactId,
12976
- ...(selectedConnectorId ? { selectedConnectorId } : {}),
12977
- };
12978
- if (command.json) {
12979
- io.stdout(JSON.stringify(payload, null, 2));
12980
- }
12981
- else {
12982
- io.stdout(shopifyWorkspaceTextLines(command, payload).join('\n'));
12983
- }
12984
- return 1;
12985
- }
12986
- const result = await requestShopifyWorkspaceSourceCommand({
12987
- command: command.command,
12988
- baseUrl,
12989
- runtimeArtifactId,
12990
- fetcher: runtime.fetcher ?? fetch,
12991
- connectorId: resolvedConnector.connectorId,
12992
- sourceArtifactId: resolvedConnector.sourceArtifactId,
12993
- });
12994
- const payload = {
12995
- ...result.payload,
12996
- command: command.command,
12997
- runtimeArtifactId,
12998
- selectedConnectorId: resolvedConnector.connectorId,
12999
- };
13000
- if (command.json) {
13001
- io.stdout(JSON.stringify(payload, null, 2));
13002
- }
13003
- else {
13004
- io.stdout(shopifyWorkspaceTextLines(command, payload).join('\n'));
13005
- }
13006
- return shopifyWorkspaceCommandSucceeded(command, payload) ? 0 : 1;
13007
- }
13008
- function shopifyWorkspaceRequestSucceeded(result) {
13009
- return result.status >= 200 && result.status < 300 && result.payload.ok !== false;
13010
- }
13011
- function shopifyWorkspaceConnectorListTextLines(payload) {
13012
- const connectors = readShopifyWorkspaceConnectorRecords(payload);
13013
- const lines = [
13014
- `runtime_artifact_id: ${String(payload.runtimeArtifactId ?? '')}`,
13015
- `shop_domain: ${String(payload.shopDomain ?? '')}`,
13016
- `connectors: ${connectors.length}`,
13017
- ];
13018
- for (const connector of connectors) {
13019
- lines.push([
13020
- String(connector.connectorId ?? ''),
13021
- String(connector.label ?? connector.appName ?? ''),
13022
- connector.linked ? 'linked' : 'not_linked',
13023
- connector.ready ? 'ready' : 'needs_setup',
13024
- ]
13025
- .filter(Boolean)
13026
- .join(' | '));
13027
- }
13028
- if (typeof payload.reasonCode === 'string') {
13029
- lines.push(`reason: ${payload.reasonCode}`);
13030
- }
13031
- return lines.filter((line) => !line.endsWith(': '));
13032
- }
13033
- function shopifyWorkspaceLinkRuntimeTextLines(payload) {
13034
- const connector = payload.connector && typeof payload.connector === 'object' && !Array.isArray(payload.connector)
13035
- ? payload.connector
13036
- : {};
13037
- const source = payload.source && typeof payload.source === 'object' && !Array.isArray(payload.source)
13038
- ? payload.source
13039
- : {};
13040
- const workspace = payload.workspace && typeof payload.workspace === 'object' && !Array.isArray(payload.workspace)
13041
- ? payload.workspace
13042
- : {};
13043
- return [
13044
- `link: ${payload.ok === false ? 'failed' : 'created'}`,
13045
- `runtime_artifact_id: ${String(payload.runtimeArtifactId ?? '')}`,
13046
- `connector_id: ${String(connector.connectorId ?? '')}`,
13047
- `source_artifact_id: ${String(source.sourceArtifactId ?? '')}`,
13048
- workspace.reloadRequired ? 'next: reload the hosted workspace to mount /workspace/shopify-connector' : '',
13049
- ...(payload.reasonCode ? [`reason: ${String(payload.reasonCode)}`] : []),
13050
- ].filter(Boolean);
13051
- }
13052
- async function runShopifyWorkspaceConnectorsList(command, io, runtime) {
13053
- const { baseUrl, runtimeArtifactId } = readShopifyWorkspaceRuntimeIdentity(runtime);
13054
- const result = await requestShopifyWorkspaceConnectorsList({
13055
- baseUrl,
13056
- runtimeArtifactId,
13057
- fetcher: runtime.fetcher ?? fetch,
13058
- });
13059
- const payload = {
13060
- ...result.payload,
13061
- command: command.command,
13062
- runtimeArtifactId,
13063
- };
13064
- if (command.json) {
13065
- io.stdout(JSON.stringify(payload, null, 2));
13066
- }
13067
- else {
13068
- io.stdout(shopifyWorkspaceConnectorListTextLines(payload).join('\n'));
13069
- }
13070
- return shopifyWorkspaceRequestSucceeded(result) ? 0 : 1;
13071
- }
13072
- async function runShopifyWorkspaceLinkRuntime(command, io, runtime) {
13073
- const { baseUrl, runtimeArtifactId } = readShopifyWorkspaceRuntimeIdentity(runtime);
13074
- const result = await requestShopifyWorkspaceLinkRuntime({
13075
- baseUrl,
13076
- runtimeArtifactId,
13077
- connectorId: command.connectorId,
13078
- fetcher: runtime.fetcher ?? fetch,
13079
- });
13080
- if (shopifyWorkspaceRequestSucceeded(result)) {
13081
- await writeHostedWorkspaceConnectorSelection({
13082
- runtimeArtifactId,
13083
- connectorId: command.connectorId,
13084
- }, runtime);
13085
- }
13086
- const payload = {
13087
- ...result.payload,
13088
12608
  command: command.command,
13089
- runtimeArtifactId,
13090
12609
  connectorId: command.connectorId,
13091
- };
13092
- if (command.json) {
13093
- io.stdout(JSON.stringify(payload, null, 2));
13094
- }
13095
- else {
13096
- io.stdout(shopifyWorkspaceLinkRuntimeTextLines(payload).join('\n'));
13097
- }
13098
- return shopifyWorkspaceRequestSucceeded(result) ? 0 : 1;
12610
+ sourceArtifactId,
12611
+ dir,
12612
+ exitCode: result.exitCode,
12613
+ stdout: result.stdout,
12614
+ stderr: result.stderr,
12615
+ }, [`connector_id: ${command.connectorId}`, `source_artifact_id: ${sourceArtifactId}`, `source_dir: ${dir}`, 'deploy: completed']);
12616
+ return 0;
13099
12617
  }
13100
12618
  async function runConnectorsShopifyDelete(command, io, runtime) {
13101
12619
  if (command.confirm !== 'delete') {
@@ -13109,7 +12627,10 @@ async function runConnectorsShopifyDelete(command, io, runtime) {
13109
12627
  token: credentials.token,
13110
12628
  fetcher: runtime.fetcher ?? fetch,
13111
12629
  });
13112
- printConnectorsPayload(command, io, { ...payload, connectorId: command.connectorId }, [`connector_id: ${command.connectorId}`, 'status: deleted']);
12630
+ printConnectorsPayload(command, io, { ...payload, connectorId: command.connectorId }, [
12631
+ `connector_id: ${command.connectorId}`,
12632
+ 'status: deleted',
12633
+ ]);
13113
12634
  return 0;
13114
12635
  }
13115
12636
  function runConnectorsBind(command, io) {
@@ -14148,14 +13669,6 @@ export async function runTenderCli(args, io = {
14148
13669
  else if (command.topic === 'connectors bind') {
14149
13670
  io.stdout(connectorsBindHelp());
14150
13671
  }
14151
- else if (command.topic === 'shopify' ||
14152
- command.topic === 'shopify workspace' ||
14153
- command.topic === 'shopify workspace connectors list' ||
14154
- command.topic === 'shopify workspace link-runtime' ||
14155
- command.topic === 'shopify workspace validate-source' ||
14156
- command.topic === 'shopify workspace deploy-source') {
14157
- io.stdout(shopifyWorkspaceHelp());
14158
- }
14159
13672
  else if (command.topic === 'auth') {
14160
13673
  io.stdout(authHelp());
14161
13674
  }
@@ -14416,15 +13929,6 @@ export async function runTenderCli(args, io = {
14416
13929
  if (command.command === 'connectors shopify source deploy') {
14417
13930
  return await runConnectorsShopifySourceDeploy(command, io, runtime);
14418
13931
  }
14419
- if (command.command === 'shopify workspace validate-source' || command.command === 'shopify workspace deploy-source') {
14420
- return await runShopifyWorkspaceSourceCommand(command, io, runtime);
14421
- }
14422
- if (command.command === 'shopify workspace connectors list') {
14423
- return await runShopifyWorkspaceConnectorsList(command, io, runtime);
14424
- }
14425
- if (command.command === 'shopify workspace link-runtime') {
14426
- return await runShopifyWorkspaceLinkRuntime(command, io, runtime);
14427
- }
14428
13932
  if (command.command === 'connectors shopify delete') {
14429
13933
  return await runConnectorsShopifyDelete(command, io, runtime);
14430
13934
  }
@@ -14597,7 +14101,10 @@ export async function runTenderCli(args, io = {
14597
14101
  }
14598
14102
  catch (error) {
14599
14103
  if (args.includes('--json') &&
14600
- ((args[0] === 'app' && (args[1] === 'db' || (args[1] === 'agent' && args[2] === 'heartbeat'))) || args[0] === 'playbooks' || args[0] === 'connectors' || args[0] === 'shopify')) {
14104
+ ((args[0] === 'app' && (args[1] === 'db' || (args[1] === 'agent' && args[2] === 'heartbeat'))) ||
14105
+ args[0] === 'playbooks' ||
14106
+ args[0] === 'connectors' ||
14107
+ args[0] === 'shopify')) {
14601
14108
  io.stdout(JSON.stringify(cliErrorPayload(error, args, command), null, 2));
14602
14109
  return error instanceof TenderCliUsageError ? 2 : 1;
14603
14110
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenderprompt/cli",
3
- "version": "0.1.35",
3
+ "version": "0.1.36",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "tender": "bin/tender.js"