@tenderprompt/cli 0.1.35 → 0.1.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -4
- package/dist/index.js +322 -710
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
import { readFileSync } from 'node:fs';
|
|
4
5
|
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
5
6
|
import os from 'node:os';
|
|
@@ -164,6 +165,12 @@ class TenderCliChildProcessError extends Error {
|
|
|
164
165
|
this.stderr = input.stderr;
|
|
165
166
|
}
|
|
166
167
|
}
|
|
168
|
+
class TenderCliDependencyInstallError extends TenderCliChildProcessError {
|
|
169
|
+
constructor(input) {
|
|
170
|
+
super(input);
|
|
171
|
+
this.name = 'TenderCliDependencyInstallError';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
167
174
|
const DEFAULT_PROFILE_NAME = 'default';
|
|
168
175
|
const DEFAULT_BASE_URL = 'https://app.tenderprompt.com';
|
|
169
176
|
const ARTIFACT_SOURCE_EXPORT_MAX_FILES = 1000;
|
|
@@ -199,12 +206,6 @@ Commands:
|
|
|
199
206
|
tender connectors shopify verify
|
|
200
207
|
Exchange credentials for an Admin API access token
|
|
201
208
|
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
209
|
tender app list List Tender App records visible to the current token
|
|
209
210
|
tender app create --name <name>
|
|
210
211
|
Create a new Tender App in the current account
|
|
@@ -282,9 +283,6 @@ Examples:
|
|
|
282
283
|
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
284
|
tender connectors shopify verify conn_123 --profile account --json
|
|
284
285
|
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
286
|
tender app list --json
|
|
289
287
|
tender app create --name "Exit Intent Widget" --json
|
|
290
288
|
tender app create --name "Data Collection App" --init --dir ./data-collection-app --preview --json
|
|
@@ -499,33 +497,6 @@ Examples:
|
|
|
499
497
|
tender connectors shopify source deploy conn_123 --dir ./shopify-app --confirm deploy --profile account --json
|
|
500
498
|
tender connectors bind SHOPIFY_CONNECTOR conn_123 --instance default --json`;
|
|
501
499
|
}
|
|
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
500
|
function connectorsShopifyCreateHelp() {
|
|
530
501
|
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
502
|
|
|
@@ -611,7 +582,10 @@ function connectorsShopifySourceValidateHelp() {
|
|
|
611
582
|
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
583
|
|
|
613
584
|
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.
|
|
585
|
+
the --execute flag, runs npm exec --yes --package @shopify/cli@latest -- shopify app config validate with npm lifecycle environment variables scrubbed. If
|
|
586
|
+
SHOPIFY_APP_AUTOMATION_TOKEN is not set locally, the CLI requests the
|
|
587
|
+
connector's stored App Automation Token and injects it only into the spawned
|
|
588
|
+
Shopify CLI child process.
|
|
615
589
|
|
|
616
590
|
Examples:
|
|
617
591
|
tender connectors shopify source validate conn_123 --dir ./shopify-app --execute --json`;
|
|
@@ -625,12 +599,9 @@ SHOPIFY_APP_AUTOMATION_TOKEN is not set locally, the CLI requests the
|
|
|
625
599
|
connector's stored App Automation Token and injects it only into the spawned
|
|
626
600
|
Shopify CLI child process.
|
|
627
601
|
|
|
628
|
-
Inside
|
|
629
|
-
|
|
630
|
-
|
|
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.
|
|
602
|
+
Inside the Shopify app, deploy connector source with the explicit **Deploy
|
|
603
|
+
source** action after the chat editor validates the pushed commit. Never ask the
|
|
604
|
+
merchant to paste token material into chat or a terminal.
|
|
634
605
|
|
|
635
606
|
Examples:
|
|
636
607
|
tender connectors shopify source deploy conn_123 --dir ./shopify-app --version connector-source-smoke --confirm deploy --profile account --json`;
|
|
@@ -1008,7 +979,8 @@ function analyticsChartsTemplatesHelp() {
|
|
|
1008
979
|
|
|
1009
980
|
Print reusable chart spec templates for agents. Adapt templates to observed
|
|
1010
981
|
events, paths, and property keys from analytics capabilities before querying or
|
|
1011
|
-
saving.
|
|
982
|
+
saving. The hourly_dimension_breakdown_24h template documents the bounded
|
|
983
|
+
hour-by-property operator report.
|
|
1012
984
|
|
|
1013
985
|
Examples:
|
|
1014
986
|
tender app analytics charts templates --json`;
|
|
@@ -1655,7 +1627,6 @@ function tenderCliCapabilities() {
|
|
|
1655
1627
|
'tender app outbound-auth --help',
|
|
1656
1628
|
'tender connectors --help',
|
|
1657
1629
|
'tender connectors shopify source init --help',
|
|
1658
|
-
'tender shopify workspace --help',
|
|
1659
1630
|
],
|
|
1660
1631
|
},
|
|
1661
1632
|
workflows: [
|
|
@@ -1700,36 +1671,6 @@ function tenderCliCapabilities() {
|
|
|
1700
1671
|
'Fetch shopify-connector-customer-metadata-widget when the requested job involves customer metafields, metaobjects, or profile writes.',
|
|
1701
1672
|
],
|
|
1702
1673
|
},
|
|
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
1674
|
{
|
|
1734
1675
|
name: 'create_new_app_with_preview',
|
|
1735
1676
|
when: 'When the user wants a new Tender App that is immediately visible.',
|
|
@@ -2127,6 +2068,25 @@ function normalizeBaseUrl(value) {
|
|
|
2127
2068
|
function readDefaultBaseUrl(env) {
|
|
2128
2069
|
return normalizeBaseUrl(env.TENDER_BASE_URL ?? DEFAULT_BASE_URL);
|
|
2129
2070
|
}
|
|
2071
|
+
function readDefaultShopifyApprovalContext(env) {
|
|
2072
|
+
const shopifyAppUrl = env.TENDER_SHOPIFY_APP_URL?.trim();
|
|
2073
|
+
if (shopifyAppUrl) {
|
|
2074
|
+
const parsed = parseShopifyAppUrl(shopifyAppUrl, 'TENDER_SHOPIFY_APP_URL');
|
|
2075
|
+
return {
|
|
2076
|
+
shopifyAppUrl: parsed.appUrl,
|
|
2077
|
+
shopifyStore: parsed.store,
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
const shopifyStore = env.TENDER_SHOPIFY_STORE?.trim();
|
|
2081
|
+
if (shopifyStore) {
|
|
2082
|
+
const store = parseShopifyStoreHandle(shopifyStore, 'TENDER_SHOPIFY_STORE');
|
|
2083
|
+
return {
|
|
2084
|
+
shopifyAppUrl: shopifyAppUrlForStore(store),
|
|
2085
|
+
shopifyStore: store,
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
2088
|
+
return null;
|
|
2089
|
+
}
|
|
2130
2090
|
function readConfigPath(env) {
|
|
2131
2091
|
return env.TENDER_CONFIG ? path.resolve(env.TENDER_CONFIG) : path.join(os.homedir(), '.tender', 'config.json');
|
|
2132
2092
|
}
|
|
@@ -2318,17 +2278,6 @@ async function resolveApiCredentials(input) {
|
|
|
2318
2278
|
};
|
|
2319
2279
|
}
|
|
2320
2280
|
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
2281
|
return await resolveApiCredentials(input);
|
|
2333
2282
|
}
|
|
2334
2283
|
function apiHeadersForCredentials(input) {
|
|
@@ -4036,59 +3985,18 @@ function parseConnectorsShopifySourceDeployArgs(args) {
|
|
|
4036
3985
|
}
|
|
4037
3986
|
throw new TenderCliUsageError(`Unknown connectors shopify source deploy option: ${arg}`);
|
|
4038
3987
|
}
|
|
4039
|
-
return {
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
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 };
|
|
3988
|
+
return {
|
|
3989
|
+
command: 'connectors shopify source deploy',
|
|
3990
|
+
connectorId,
|
|
3991
|
+
dir,
|
|
3992
|
+
clientId,
|
|
3993
|
+
version,
|
|
3994
|
+
message,
|
|
3995
|
+
sourceControlUrl,
|
|
3996
|
+
allowDeletes,
|
|
3997
|
+
confirm,
|
|
3998
|
+
...parsed,
|
|
3999
|
+
};
|
|
4092
4000
|
}
|
|
4093
4001
|
function parseConnectorsShopifyDeleteArgs(args) {
|
|
4094
4002
|
if (args.includes('--help') || args.includes('-h')) {
|
|
@@ -5614,6 +5522,10 @@ function analyticsAuthoringContract() {
|
|
|
5614
5522
|
},
|
|
5615
5523
|
],
|
|
5616
5524
|
charting: {
|
|
5525
|
+
contractDiscovery: {
|
|
5526
|
+
capabilities: 'tender app analytics capabilities <app-id> --include-catalog --json',
|
|
5527
|
+
templates: 'tender app analytics charts templates --json',
|
|
5528
|
+
},
|
|
5617
5529
|
starterDashboardCommands: ANALYTICS_AGENT_GUIDANCE.starterDashboardCommands,
|
|
5618
5530
|
completionProof: ANALYTICS_AGENT_GUIDANCE.dashboardCompletion.proofCommand,
|
|
5619
5531
|
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 +5763,33 @@ function analyticsChartTemplates() {
|
|
|
5851
5763
|
limit: 10,
|
|
5852
5764
|
},
|
|
5853
5765
|
},
|
|
5766
|
+
{
|
|
5767
|
+
id: 'hourly_dimension_breakdown_24h',
|
|
5768
|
+
title: 'Hourly dimension breakdown (24h)',
|
|
5769
|
+
visualization: 'table',
|
|
5770
|
+
purpose: 'Compare one low-cardinality promoted dimension by hour over the last 24 hours.',
|
|
5771
|
+
usableAsIs: false,
|
|
5772
|
+
placeholders: {
|
|
5773
|
+
eventName: '<event_name>',
|
|
5774
|
+
property: '<queryable_dimension_property>',
|
|
5775
|
+
},
|
|
5776
|
+
adaptWith: [
|
|
5777
|
+
'Replace both placeholders with one observed custom event and one active dimension property for that event.',
|
|
5778
|
+
'Use only non-sensitive categorical dimensions with at most 16 observed values.',
|
|
5779
|
+
'Keep the 24h range, hour-first grouping order, table visualization, custom_event_count metric, and 400-row limit unchanged.',
|
|
5780
|
+
],
|
|
5781
|
+
spec: {
|
|
5782
|
+
version: 1,
|
|
5783
|
+
dataset: 'events',
|
|
5784
|
+
visualization: 'table',
|
|
5785
|
+
metric: { name: 'custom_event_count' },
|
|
5786
|
+
groupBy: [{ field: 'hour' }, { field: 'property:<queryable_dimension_property>' }],
|
|
5787
|
+
filters: [{ field: 'event_name', op: '=', value: '<event_name>' }],
|
|
5788
|
+
range: { preset: '24h' },
|
|
5789
|
+
sort: [{ field: 'hour', direction: 'asc' }],
|
|
5790
|
+
limit: 400,
|
|
5791
|
+
},
|
|
5792
|
+
},
|
|
5854
5793
|
{
|
|
5855
5794
|
id: 'experiment_readout',
|
|
5856
5795
|
title: 'Experiment readout',
|
|
@@ -5871,10 +5810,7 @@ function analyticsChartTemplates() {
|
|
|
5871
5810
|
dataset: 'events',
|
|
5872
5811
|
visualization: 'table',
|
|
5873
5812
|
metric: { name: 'custom_event_count' },
|
|
5874
|
-
groupBy: [
|
|
5875
|
-
{ field: 'event_name' },
|
|
5876
|
-
{ field: 'property:experiment_variant' },
|
|
5877
|
-
],
|
|
5813
|
+
groupBy: [{ field: 'event_name' }, { field: 'property:experiment_variant' }],
|
|
5878
5814
|
filters: [
|
|
5879
5815
|
{
|
|
5880
5816
|
field: 'event_name',
|
|
@@ -7185,30 +7121,6 @@ function parseTenderArgs(args) {
|
|
|
7185
7121
|
}
|
|
7186
7122
|
throw new TenderCliUsageError(`Unknown connectors command: ${args[1]}`);
|
|
7187
7123
|
}
|
|
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
7124
|
if (args[0] === 'apps') {
|
|
7213
7125
|
throw new TenderCliUsageError('Unknown command: apps. Use `tender app ...` for Tender App workflows.');
|
|
7214
7126
|
}
|
|
@@ -7274,7 +7186,8 @@ function parseTenderArgs(args) {
|
|
|
7274
7186
|
if (resourceArgs[1] === 'bindings' && (resourceArgs[2] === undefined || resourceArgs[2] === '--help' || resourceArgs[2] === '-h')) {
|
|
7275
7187
|
return { command: 'help', topic: 'app bindings outbound' };
|
|
7276
7188
|
}
|
|
7277
|
-
if (resourceArgs[1] === 'outbound-auth' &&
|
|
7189
|
+
if (resourceArgs[1] === 'outbound-auth' &&
|
|
7190
|
+
(resourceArgs[2] === undefined || resourceArgs[2] === '--help' || resourceArgs[2] === '-h')) {
|
|
7278
7191
|
return { command: 'help', topic: 'app outbound-auth' };
|
|
7279
7192
|
}
|
|
7280
7193
|
if (resourceArgs[1] === 'outbound-auth' && resourceArgs[2] === 'list') {
|
|
@@ -7884,12 +7797,7 @@ async function requestAppDbDashboardApi(input) {
|
|
|
7884
7797
|
(typeof payload?.reasonCode === 'string' && payload.reasonCode) ||
|
|
7885
7798
|
`App database dashboard request failed with HTTP ${response.status}.`;
|
|
7886
7799
|
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, {
|
|
7800
|
+
throw new TenderCliDbApiError([message, `Scope: artifact ${input.artifactId}, app-db-dashboards.`, `Reason: ${reasonCode}.`, `Example: ${example}.`].join('\n'), reasonCode, {
|
|
7893
7801
|
artifactId: input.artifactId,
|
|
7894
7802
|
surface: 'db/dashboards',
|
|
7895
7803
|
}, example);
|
|
@@ -7945,189 +7853,13 @@ async function requestConnectorsApi(input) {
|
|
|
7945
7853
|
const message = (typeof payload?.message === 'string' && payload.message) ||
|
|
7946
7854
|
(typeof payload?.reasonCode === 'string' && payload.reasonCode) ||
|
|
7947
7855
|
`Connector request failed with HTTP ${response.status}.`;
|
|
7948
|
-
const example =
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
|
|
7856
|
+
const example = 'Example: tender connectors list --profile account --json.';
|
|
7857
|
+
throw new TenderCliConnectorsApiError([message, `Reason: ${reasonCode}.`, example].join('\n'), reasonCode, response.status, payload ?? null, example
|
|
7858
|
+
.replace(/^Example:\s*/i, '')
|
|
7859
|
+
.replace(/\.$/, ''));
|
|
7952
7860
|
}
|
|
7953
7861
|
return payload;
|
|
7954
7862
|
}
|
|
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
7863
|
function dbCorrectedExample(artifactId, dbPath) {
|
|
8132
7864
|
if (dbPath === 'capabilities') {
|
|
8133
7865
|
return `tender app db capabilities ${artifactId} --json`;
|
|
@@ -8242,26 +7974,29 @@ function readAnalyticsSpecMetricName(spec) {
|
|
|
8242
7974
|
}
|
|
8243
7975
|
return readCliStringField(spec.metric, 'name');
|
|
8244
7976
|
}
|
|
8245
|
-
function
|
|
7977
|
+
function readAnalyticsSpecGroupFields(spec) {
|
|
8246
7978
|
if (!isCliJsonRecord(spec) || !Array.isArray(spec.groupBy)) {
|
|
8247
|
-
return
|
|
7979
|
+
return [];
|
|
8248
7980
|
}
|
|
8249
|
-
|
|
8250
|
-
|
|
7981
|
+
return spec.groupBy
|
|
7982
|
+
.filter(isCliJsonRecord)
|
|
7983
|
+
.map((group) => readCliStringField(group, 'field'))
|
|
7984
|
+
.filter((field) => field !== null);
|
|
8251
7985
|
}
|
|
8252
7986
|
function readRequiredAnalyticsPropertiesFromSpec(spec) {
|
|
8253
7987
|
const eventName = readAnalyticsSpecEventNameFilter(spec);
|
|
8254
7988
|
const requirements = [];
|
|
8255
|
-
const groupField
|
|
8256
|
-
|
|
8257
|
-
|
|
8258
|
-
|
|
7989
|
+
for (const groupField of readAnalyticsSpecGroupFields(spec)) {
|
|
7990
|
+
if (groupField.startsWith('property:')) {
|
|
7991
|
+
if (!eventName) {
|
|
7992
|
+
throw new TenderCliUsageError('Property group-by chart specs require an event_name = filter so the CLI can make the property available to dashboard queries.');
|
|
7993
|
+
}
|
|
7994
|
+
requirements.push({
|
|
7995
|
+
eventName,
|
|
7996
|
+
propertyKey: groupField.slice('property:'.length),
|
|
7997
|
+
propertyType: 'dimension',
|
|
7998
|
+
});
|
|
8259
7999
|
}
|
|
8260
|
-
requirements.push({
|
|
8261
|
-
eventName,
|
|
8262
|
-
propertyKey: groupField.slice('property:'.length),
|
|
8263
|
-
propertyType: 'dimension',
|
|
8264
|
-
});
|
|
8265
8000
|
}
|
|
8266
8001
|
const metricName = readAnalyticsSpecMetricName(spec);
|
|
8267
8002
|
if (metricName?.startsWith('sum:') || metricName?.startsWith('avg:')) {
|
|
@@ -8432,7 +8167,16 @@ function csvCell(value) {
|
|
|
8432
8167
|
return text;
|
|
8433
8168
|
}
|
|
8434
8169
|
function rowsToCsv(rows) {
|
|
8435
|
-
const records = rows
|
|
8170
|
+
const records = rows
|
|
8171
|
+
.filter((row) => Boolean(row) && typeof row === 'object' && !Array.isArray(row))
|
|
8172
|
+
.map((row) => {
|
|
8173
|
+
const dimensions = row.dimensions;
|
|
8174
|
+
if (!dimensions || typeof dimensions !== 'object' || Array.isArray(dimensions)) {
|
|
8175
|
+
return row;
|
|
8176
|
+
}
|
|
8177
|
+
const { dimensions: _dimensions, ...rest } = row;
|
|
8178
|
+
return { ...dimensions, ...rest };
|
|
8179
|
+
});
|
|
8436
8180
|
const headers = Array.from(records.reduce((set, row) => {
|
|
8437
8181
|
for (const key of Object.keys(row)) {
|
|
8438
8182
|
set.add(key);
|
|
@@ -9058,11 +8802,13 @@ async function runAuthLogin(command, io, runtime) {
|
|
|
9058
8802
|
const fetcher = runtime.fetcher ?? fetch;
|
|
9059
8803
|
const sleeper = runtime.sleeper ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
|
|
9060
8804
|
const now = runtime.now ?? Date.now;
|
|
8805
|
+
const agentAutoPoll = command.json && env.TENDER_AGENT_DEVICE_AUTH_AUTO_POLL === '1';
|
|
9061
8806
|
const config = await readConfig(env);
|
|
9062
8807
|
const existingProfile = config.profiles?.[command.saveProfile] ?? null;
|
|
9063
8808
|
const baseUrl = command.baseUrl ?? existingProfile?.baseUrl ?? readDefaultBaseUrl(env);
|
|
9064
|
-
const
|
|
9065
|
-
const
|
|
8809
|
+
const defaultShopifyApprovalContext = readDefaultShopifyApprovalContext(env);
|
|
8810
|
+
const shopifyAppUrl = command.shopifyAppUrl ?? existingProfile?.shopifyAppUrl ?? defaultShopifyApprovalContext?.shopifyAppUrl ?? null;
|
|
8811
|
+
const shopifyStore = command.shopifyStore ?? existingProfile?.shopifyStore ?? defaultShopifyApprovalContext?.shopifyStore ?? null;
|
|
9066
8812
|
const deviceResponse = await fetcher(`${baseUrl}/oauth/device/code`, {
|
|
9067
8813
|
method: 'POST',
|
|
9068
8814
|
headers: {
|
|
@@ -9135,7 +8881,7 @@ async function runAuthLogin(command, io, runtime) {
|
|
|
9135
8881
|
configPath: readConfigPath(env),
|
|
9136
8882
|
nextCommand: buildAuthStatusNextCommand(command.saveProfile),
|
|
9137
8883
|
};
|
|
9138
|
-
if (command.noPoll) {
|
|
8884
|
+
if (command.noPoll && !agentAutoPoll) {
|
|
9139
8885
|
if (command.json) {
|
|
9140
8886
|
io.stdout(JSON.stringify(pendingResult, null, 2));
|
|
9141
8887
|
}
|
|
@@ -9148,10 +8894,7 @@ async function runAuthLogin(command, io, runtime) {
|
|
|
9148
8894
|
io.stderr(JSON.stringify(pendingResult, null, 2));
|
|
9149
8895
|
}
|
|
9150
8896
|
else {
|
|
9151
|
-
io.stdout([
|
|
9152
|
-
`Open this URL to approve Tender CLI access: ${verificationUriComplete}`,
|
|
9153
|
-
`User code: ${devicePayload.user_code}`,
|
|
9154
|
-
].join('\n'));
|
|
8897
|
+
io.stdout([`Open this URL to approve Tender CLI access: ${verificationUriComplete}`, `User code: ${devicePayload.user_code}`].join('\n'));
|
|
9155
8898
|
}
|
|
9156
8899
|
let intervalSeconds = Math.max(1, devicePayload.interval ?? 5);
|
|
9157
8900
|
let attemptsRemaining = Math.max(1, Math.ceil(command.pollTimeoutSeconds / intervalSeconds));
|
|
@@ -9848,12 +9591,7 @@ async function resolveGitCommitArgument(input) {
|
|
|
9848
9591
|
return commitSha.toLowerCase();
|
|
9849
9592
|
}
|
|
9850
9593
|
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'));
|
|
9594
|
+
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
9595
|
}
|
|
9858
9596
|
function readCommandCommitSha(command) {
|
|
9859
9597
|
const commitSha = 'commitSha' in command ? command.commitSha : null;
|
|
@@ -10824,9 +10562,7 @@ async function postLifecycle(input) {
|
|
|
10824
10562
|
},
|
|
10825
10563
|
runtime: input.runtime,
|
|
10826
10564
|
});
|
|
10827
|
-
const memorySync = input.operation === 'publish'
|
|
10828
|
-
? await flushAutomaticMemory({ repoPath, remoteName: 'tender', runtime: input.runtime })
|
|
10829
|
-
: null;
|
|
10565
|
+
const memorySync = input.operation === 'publish' ? await flushAutomaticMemory({ repoPath, remoteName: 'tender', runtime: input.runtime }) : null;
|
|
10830
10566
|
const resultWithMemory = {
|
|
10831
10567
|
...result,
|
|
10832
10568
|
memory,
|
|
@@ -11153,7 +10889,7 @@ async function runOutboundAuthApprove(command, io, runtime) {
|
|
|
11153
10889
|
});
|
|
11154
10890
|
const fetcher = runtime.fetcher ?? fetch;
|
|
11155
10891
|
const grants = command.approveAll
|
|
11156
|
-
? (await requestOutboundAuthList({ command, credentials, fetcher })).grants?.filter((grant) => !grant.approved) ?? []
|
|
10892
|
+
? ((await requestOutboundAuthList({ command, credentials, fetcher })).grants?.filter((grant) => !grant.approved) ?? [])
|
|
11157
10893
|
: [readExplicitOutboundAuthGrant(command)];
|
|
11158
10894
|
const approved = [];
|
|
11159
10895
|
for (const grant of grants) {
|
|
@@ -11173,11 +10909,7 @@ async function runOutboundAuthApprove(command, io, runtime) {
|
|
|
11173
10909
|
io.stdout(JSON.stringify(result, null, 2));
|
|
11174
10910
|
}
|
|
11175
10911
|
else {
|
|
11176
|
-
io.stdout([
|
|
11177
|
-
`app_id: ${command.artifactId}`,
|
|
11178
|
-
`approved: ${approved.length}`,
|
|
11179
|
-
...approved.map((grant) => formatOutboundAuthGrant(grant)),
|
|
11180
|
-
].join('\n'));
|
|
10912
|
+
io.stdout([`app_id: ${command.artifactId}`, `approved: ${approved.length}`, ...approved.map((grant) => formatOutboundAuthGrant(grant))].join('\n'));
|
|
11181
10913
|
}
|
|
11182
10914
|
return 0;
|
|
11183
10915
|
}
|
|
@@ -11596,7 +11328,9 @@ async function runMemoryBrief(command, io, runtime) {
|
|
|
11596
11328
|
const unresolved = entries
|
|
11597
11329
|
.filter((entry) => entry.status === 'open' || entry.kind === 'failure' || entry.kind === 'open_question' || entry.kind === 'support_handoff')
|
|
11598
11330
|
.slice(-command.limit);
|
|
11599
|
-
const briefEntries = latestSummary
|
|
11331
|
+
const briefEntries = latestSummary
|
|
11332
|
+
? [latestSummary, ...unresolved.filter((entry) => entry.entryId !== latestSummary.entryId)]
|
|
11333
|
+
: unresolved;
|
|
11600
11334
|
const result = {
|
|
11601
11335
|
ok: true,
|
|
11602
11336
|
command: command.command,
|
|
@@ -11632,11 +11366,7 @@ async function runMemoryFlush(command, io, runtime) {
|
|
|
11632
11366
|
io.stdout(JSON.stringify(result, null, 2));
|
|
11633
11367
|
}
|
|
11634
11368
|
else {
|
|
11635
|
-
io.stdout([
|
|
11636
|
-
`memory_sync: ${sync.status}`,
|
|
11637
|
-
sync.error ? `error: ${sync.error}` : null,
|
|
11638
|
-
result.next ? `next: ${result.next}` : null,
|
|
11639
|
-
]
|
|
11369
|
+
io.stdout([`memory_sync: ${sync.status}`, sync.error ? `error: ${sync.error}` : null, result.next ? `next: ${result.next}` : null]
|
|
11640
11370
|
.filter((line) => Boolean(line))
|
|
11641
11371
|
.join('\n'));
|
|
11642
11372
|
}
|
|
@@ -11854,7 +11584,6 @@ async function runConnectorsList(command, io, runtime) {
|
|
|
11854
11584
|
baseUrl: credentials.baseUrl,
|
|
11855
11585
|
token: credentials.token,
|
|
11856
11586
|
fetcher: runtime.fetcher ?? fetch,
|
|
11857
|
-
hostedWorkspaceHint: Boolean((runtime.env ?? process.env).TENDER_RUNTIME_ARTIFACT_ID),
|
|
11858
11587
|
});
|
|
11859
11588
|
const connectors = readConnectorArray(payload);
|
|
11860
11589
|
printConnectorsPayload(command, io, payload, connectors.length > 0
|
|
@@ -11907,10 +11636,7 @@ async function runConnectorsShopifySetupUrl(command, io, runtime) {
|
|
|
11907
11636
|
token: credentials.token,
|
|
11908
11637
|
fetcher: runtime.fetcher ?? fetch,
|
|
11909
11638
|
});
|
|
11910
|
-
printConnectorsPayload(command, io, payload, [
|
|
11911
|
-
`connector_id: ${command.connectorId}`,
|
|
11912
|
-
`setup_url: ${String(payload.setupUrl ?? '')}`,
|
|
11913
|
-
]);
|
|
11639
|
+
printConnectorsPayload(command, io, payload, [`connector_id: ${command.connectorId}`, `setup_url: ${String(payload.setupUrl ?? '')}`]);
|
|
11914
11640
|
return 0;
|
|
11915
11641
|
}
|
|
11916
11642
|
function readClientSecret(command, runtime) {
|
|
@@ -12147,7 +11873,10 @@ merchant-editable settings.
|
|
|
12147
11873
|
`;
|
|
12148
11874
|
return new Map([
|
|
12149
11875
|
['extensions/tender-product-survey/README.md', readme],
|
|
12150
|
-
[
|
|
11876
|
+
[
|
|
11877
|
+
'extensions/tender-product-survey/package.json',
|
|
11878
|
+
'{\n "name": "tender-product-survey",\n "private": true,\n "version": "0.0.0"\n}\n',
|
|
11879
|
+
],
|
|
12151
11880
|
['extensions/tender-product-survey/shopify.extension.toml', extensionToml],
|
|
12152
11881
|
['extensions/tender-product-survey/blocks/product_survey.liquid', blockLiquid],
|
|
12153
11882
|
['extensions/tender-product-survey/locales/en.default.json', '{}\n'],
|
|
@@ -12248,16 +11977,10 @@ npm exec --yes --package @shopify/cli@latest -- shopify app config validate --pa
|
|
|
12248
11977
|
tender connectors shopify source deploy ${input.connectorId} --dir . --confirm deploy --profile account --json
|
|
12249
11978
|
\`\`\`
|
|
12250
11979
|
|
|
12251
|
-
Inside
|
|
12252
|
-
|
|
12253
|
-
|
|
12254
|
-
|
|
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.
|
|
11980
|
+
Inside Shopify, request source changes in the Custom app editor. Tender
|
|
11981
|
+
validates the pushed commit, and the explicit Deploy source action uses the
|
|
11982
|
+
connector's stored App Automation Token server-side. Never ask for Shopify App
|
|
11983
|
+
Automation Tokens, client secrets, Tender tokens, or provider keys in chat.
|
|
12261
11984
|
`;
|
|
12262
11985
|
const files = new Map([
|
|
12263
11986
|
['.gitignore', '.shopify/\nnode_modules/\n.env\n.env.*\n'],
|
|
@@ -12500,15 +12223,7 @@ async function commitAndPushSourceFiles(input) {
|
|
|
12500
12223
|
};
|
|
12501
12224
|
}
|
|
12502
12225
|
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
|
-
];
|
|
12226
|
+
return ['app', 'config', 'validate', '--path', input.dir, '--json', ...(input.clientId ? ['--client-id', input.clientId] : [])];
|
|
12512
12227
|
}
|
|
12513
12228
|
function shopifyDeployArgs(input) {
|
|
12514
12229
|
return [
|
|
@@ -12580,6 +12295,85 @@ function redactSensitiveText(value, secrets = []) {
|
|
|
12580
12295
|
.replace(/\b(authorization\s*[:=]\s*(?:Bearer\s+)?)([^\s'"]+)/gi, '$1[REDACTED]')
|
|
12581
12296
|
.replace(/\b(x-shopify-access-token\s*[:=]\s*)([^\s'"]+)/gi, '$1[REDACTED]');
|
|
12582
12297
|
}
|
|
12298
|
+
const SHOPIFY_SOURCE_DEPENDENCY_STATE_VERSION = 1;
|
|
12299
|
+
const SHOPIFY_SOURCE_DEPENDENCY_STATE_FILE = '.tender-shopify-dependencies.json';
|
|
12300
|
+
function dependencyInstallChildEnv(runtime) {
|
|
12301
|
+
const env = shopifyCliChildEnv(runtime);
|
|
12302
|
+
for (const key of Object.keys(env)) {
|
|
12303
|
+
const normalized = key.toUpperCase();
|
|
12304
|
+
if (/(?:TOKEN|SECRET|PASSWORD|PASSWD|AUTHORIZATION|COOKIE|API_KEY|PRIVATE_KEY|CREDENTIAL)/.test(normalized)) {
|
|
12305
|
+
delete env[key];
|
|
12306
|
+
}
|
|
12307
|
+
}
|
|
12308
|
+
return {
|
|
12309
|
+
...env,
|
|
12310
|
+
NPM_CONFIG_USERCONFIG: '/dev/null',
|
|
12311
|
+
};
|
|
12312
|
+
}
|
|
12313
|
+
async function prepareShopifySourceDependencies(input) {
|
|
12314
|
+
const packageJsonPath = path.join(input.dir, 'package.json');
|
|
12315
|
+
if (!(await fileExists(packageJsonPath))) {
|
|
12316
|
+
return {
|
|
12317
|
+
status: 'not_required',
|
|
12318
|
+
packageManager: null,
|
|
12319
|
+
lockfile: null,
|
|
12320
|
+
installed: false,
|
|
12321
|
+
};
|
|
12322
|
+
}
|
|
12323
|
+
const lockfileName = 'package-lock.json';
|
|
12324
|
+
const lockfilePath = path.join(input.dir, lockfileName);
|
|
12325
|
+
if (!(await fileExists(lockfilePath))) {
|
|
12326
|
+
throw new TenderCliUsageError('Missing package-lock.json. Shopify connector source with package.json must commit a lockfile before validation or deployment.');
|
|
12327
|
+
}
|
|
12328
|
+
const dependencyStateHash = createHash('sha256')
|
|
12329
|
+
.update(await readFile(packageJsonPath))
|
|
12330
|
+
.update('\0')
|
|
12331
|
+
.update(await readFile(lockfilePath))
|
|
12332
|
+
.digest('hex');
|
|
12333
|
+
const statePath = path.join(input.dir, 'node_modules', SHOPIFY_SOURCE_DEPENDENCY_STATE_FILE);
|
|
12334
|
+
try {
|
|
12335
|
+
const state = JSON.parse(await readFile(statePath, 'utf8'));
|
|
12336
|
+
if (typeof state === 'object' &&
|
|
12337
|
+
state !== null &&
|
|
12338
|
+
'version' in state &&
|
|
12339
|
+
state.version === SHOPIFY_SOURCE_DEPENDENCY_STATE_VERSION &&
|
|
12340
|
+
'dependencyStateHash' in state &&
|
|
12341
|
+
state.dependencyStateHash === dependencyStateHash) {
|
|
12342
|
+
return {
|
|
12343
|
+
status: 'ready',
|
|
12344
|
+
packageManager: 'npm',
|
|
12345
|
+
lockfile: lockfileName,
|
|
12346
|
+
installed: false,
|
|
12347
|
+
};
|
|
12348
|
+
}
|
|
12349
|
+
}
|
|
12350
|
+
catch {
|
|
12351
|
+
// A missing or stale marker means the locked dependency tree must be rebuilt.
|
|
12352
|
+
}
|
|
12353
|
+
const runner = input.runtime.commandRunner ?? defaultCommandRunner;
|
|
12354
|
+
const args = ['ci', '--ignore-scripts'];
|
|
12355
|
+
const result = await runner('npm', args, {
|
|
12356
|
+
cwd: input.dir,
|
|
12357
|
+
env: dependencyInstallChildEnv(input.runtime),
|
|
12358
|
+
});
|
|
12359
|
+
if (result.exitCode !== 0) {
|
|
12360
|
+
throw new TenderCliDependencyInstallError({
|
|
12361
|
+
command: 'npm',
|
|
12362
|
+
args,
|
|
12363
|
+
exitCode: result.exitCode,
|
|
12364
|
+
stdout: redactSensitiveText(result.stdout),
|
|
12365
|
+
stderr: redactSensitiveText(result.stderr),
|
|
12366
|
+
});
|
|
12367
|
+
}
|
|
12368
|
+
await mkdir(path.dirname(statePath), { recursive: true });
|
|
12369
|
+
await writeFile(statePath, `${JSON.stringify({ version: SHOPIFY_SOURCE_DEPENDENCY_STATE_VERSION, dependencyStateHash })}\n`, 'utf8');
|
|
12370
|
+
return {
|
|
12371
|
+
status: 'ready',
|
|
12372
|
+
packageManager: 'npm',
|
|
12373
|
+
lockfile: lockfileName,
|
|
12374
|
+
installed: true,
|
|
12375
|
+
};
|
|
12376
|
+
}
|
|
12583
12377
|
async function runShopifyCliCommand(input) {
|
|
12584
12378
|
const runner = input.runtime.commandRunner ?? defaultCommandRunner;
|
|
12585
12379
|
const invocation = shopifyCliCommand(input.args);
|
|
@@ -12636,18 +12430,18 @@ function childShopifyDeployEnv(_runtime, token) {
|
|
|
12636
12430
|
function connectorsProfileFlag(command) {
|
|
12637
12431
|
return command.profileName ? `--profile ${command.profileName}` : '--profile account';
|
|
12638
12432
|
}
|
|
12639
|
-
function
|
|
12433
|
+
function shopifyCliTokenRecoveryMessage(command, reasonCode) {
|
|
12640
12434
|
const profileFlag = connectorsProfileFlag(command);
|
|
12641
12435
|
return [
|
|
12642
12436
|
reasonCode === 'shopify_deploy_token_expired'
|
|
12643
12437
|
? 'The connector stored Shopify App Automation Token is expired.'
|
|
12644
|
-
: 'The connector does not have a stored Shopify App Automation Token for source
|
|
12438
|
+
: 'The connector does not have a stored Shopify App Automation Token for source validation and deployment.',
|
|
12645
12439
|
`Store a fresh token: printf "$SHOPIFY_APP_AUTOMATION_TOKEN" | tender connectors shopify app-automation-token set ${command.connectorId} --token-stdin ${profileFlag} --json`,
|
|
12646
12440
|
`Setup URL: tender connectors shopify setup-url ${command.connectorId} ${profileFlag} --json`,
|
|
12647
|
-
'Fallback: export SHOPIFY_APP_AUTOMATION_TOKEN locally before running source
|
|
12441
|
+
'Fallback: export SHOPIFY_APP_AUTOMATION_TOKEN locally before running source validation or deployment.',
|
|
12648
12442
|
].join('\n');
|
|
12649
12443
|
}
|
|
12650
|
-
async function
|
|
12444
|
+
async function resolveShopifyCliTokenEnvironment(input) {
|
|
12651
12445
|
const localToken = localShopifyDeployToken(input.runtime);
|
|
12652
12446
|
if (localToken) {
|
|
12653
12447
|
return {
|
|
@@ -12660,7 +12454,9 @@ async function resolveShopifyDeployTokenEnvironment(input) {
|
|
|
12660
12454
|
const handoff = await fetchShopifyDeployToken({
|
|
12661
12455
|
connectorId: input.command.connectorId,
|
|
12662
12456
|
sourceArtifactId: input.sourceArtifactId,
|
|
12663
|
-
version: input.command.
|
|
12457
|
+
version: input.command.command === 'connectors shopify source deploy'
|
|
12458
|
+
? input.command.version
|
|
12459
|
+
: null,
|
|
12664
12460
|
credentials: input.credentials,
|
|
12665
12461
|
fetcher: input.fetcher,
|
|
12666
12462
|
});
|
|
@@ -12673,7 +12469,7 @@ async function resolveShopifyDeployTokenEnvironment(input) {
|
|
|
12673
12469
|
catch (error) {
|
|
12674
12470
|
if (error instanceof TenderCliConnectorsApiError &&
|
|
12675
12471
|
(error.reasonCode === 'shopify_deploy_token_required' || error.reasonCode === 'shopify_deploy_token_expired')) {
|
|
12676
|
-
const recovery =
|
|
12472
|
+
const recovery = shopifyCliTokenRecoveryMessage(input.command, error.reasonCode);
|
|
12677
12473
|
throw new TenderCliGuidedUsageError(recovery, recovery);
|
|
12678
12474
|
}
|
|
12679
12475
|
throw error;
|
|
@@ -12739,7 +12535,14 @@ async function runConnectorsShopifySourceInit(command, io, runtime) {
|
|
|
12739
12535
|
commit: gitCommit,
|
|
12740
12536
|
},
|
|
12741
12537
|
validateCommand: shopifyCliCommandLine(shopifyValidateArgs({ dir, clientId: command.clientId })),
|
|
12742
|
-
deployCommand: shopifyCliCommandLine(shopifyDeployArgs({
|
|
12538
|
+
deployCommand: shopifyCliCommandLine(shopifyDeployArgs({
|
|
12539
|
+
dir,
|
|
12540
|
+
clientId: command.clientId,
|
|
12541
|
+
version: 'connector-source',
|
|
12542
|
+
message: null,
|
|
12543
|
+
sourceControlUrl: null,
|
|
12544
|
+
allowDeletes: false,
|
|
12545
|
+
})),
|
|
12743
12546
|
};
|
|
12744
12547
|
printConnectorsPayload(command, io, result, [
|
|
12745
12548
|
`connector_id: ${command.connectorId}`,
|
|
@@ -12787,7 +12590,7 @@ async function runConnectorsShopifySourceCheckout(command, io, runtime) {
|
|
|
12787
12590
|
return 0;
|
|
12788
12591
|
}
|
|
12789
12592
|
async function runConnectorsShopifySourceValidate(command, io, runtime) {
|
|
12790
|
-
const { connector } = await requestConnector(command, runtime);
|
|
12593
|
+
const { credentials, connector } = await requestConnector(command, runtime);
|
|
12791
12594
|
const sourceArtifactId = readConnectorSourceArtifactId(connector);
|
|
12792
12595
|
if (!sourceArtifactId) {
|
|
12793
12596
|
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 +12604,54 @@ async function runConnectorsShopifySourceValidate(command, io, runtime) {
|
|
|
12801
12604
|
}
|
|
12802
12605
|
}
|
|
12803
12606
|
if (!command.execute) {
|
|
12804
|
-
printConnectorsPayload(command, io, {
|
|
12607
|
+
printConnectorsPayload(command, io, {
|
|
12608
|
+
ok: true,
|
|
12609
|
+
dryRun: true,
|
|
12610
|
+
command: command.command,
|
|
12611
|
+
connectorId: command.connectorId,
|
|
12612
|
+
sourceArtifactId,
|
|
12613
|
+
dir,
|
|
12614
|
+
shopifyCommand: shopifyCliCommandLine(args),
|
|
12615
|
+
}, [
|
|
12616
|
+
`connector_id: ${command.connectorId}`,
|
|
12617
|
+
`source_artifact_id: ${sourceArtifactId}`,
|
|
12618
|
+
`source_dir: ${dir}`,
|
|
12619
|
+
`shopify_command: ${shopifyCliCommandLine(args)}`,
|
|
12620
|
+
]);
|
|
12805
12621
|
return 0;
|
|
12806
12622
|
}
|
|
12807
|
-
const
|
|
12808
|
-
|
|
12623
|
+
const dependencies = await prepareShopifySourceDependencies({ dir, runtime });
|
|
12624
|
+
const validationToken = await resolveShopifyCliTokenEnvironment({
|
|
12625
|
+
command,
|
|
12626
|
+
sourceArtifactId,
|
|
12627
|
+
credentials,
|
|
12628
|
+
runtime,
|
|
12629
|
+
fetcher: runtime.fetcher ?? fetch,
|
|
12630
|
+
});
|
|
12631
|
+
const result = await runShopifyCliCommand({
|
|
12632
|
+
args,
|
|
12633
|
+
cwd: dir,
|
|
12634
|
+
runtime,
|
|
12635
|
+
env: validationToken.env,
|
|
12636
|
+
redactions: validationToken.redactions,
|
|
12637
|
+
});
|
|
12638
|
+
printConnectorsPayload(command, io, {
|
|
12639
|
+
ok: true,
|
|
12640
|
+
command: command.command,
|
|
12641
|
+
connectorId: command.connectorId,
|
|
12642
|
+
sourceArtifactId,
|
|
12643
|
+
dir,
|
|
12644
|
+
exitCode: result.exitCode,
|
|
12645
|
+
stdout: result.stdout,
|
|
12646
|
+
stderr: result.stderr,
|
|
12647
|
+
dependencies,
|
|
12648
|
+
}, [
|
|
12649
|
+
`connector_id: ${command.connectorId}`,
|
|
12650
|
+
`source_artifact_id: ${sourceArtifactId}`,
|
|
12651
|
+
`source_dir: ${dir}`,
|
|
12652
|
+
`dependencies: ${dependencies.status === 'ready' ? (dependencies.installed ? 'installed' : 'cached') : 'not_required'}`,
|
|
12653
|
+
'validation: passed',
|
|
12654
|
+
]);
|
|
12809
12655
|
return 0;
|
|
12810
12656
|
}
|
|
12811
12657
|
async function runConnectorsShopifySourceDeploy(command, io, runtime) {
|
|
@@ -12825,10 +12671,26 @@ async function runConnectorsShopifySourceDeploy(command, io, runtime) {
|
|
|
12825
12671
|
allowDeletes: command.allowDeletes,
|
|
12826
12672
|
});
|
|
12827
12673
|
if (command.confirm !== 'deploy') {
|
|
12828
|
-
printConnectorsPayload(command, io, {
|
|
12674
|
+
printConnectorsPayload(command, io, {
|
|
12675
|
+
ok: true,
|
|
12676
|
+
dryRun: true,
|
|
12677
|
+
command: command.command,
|
|
12678
|
+
connectorId: command.connectorId,
|
|
12679
|
+
sourceArtifactId,
|
|
12680
|
+
dir,
|
|
12681
|
+
shopifyCommand: shopifyCliCommandLine(args),
|
|
12682
|
+
confirmRequired: 'deploy',
|
|
12683
|
+
}, [
|
|
12684
|
+
`connector_id: ${command.connectorId}`,
|
|
12685
|
+
`source_artifact_id: ${sourceArtifactId}`,
|
|
12686
|
+
`source_dir: ${dir}`,
|
|
12687
|
+
`shopify_command: ${shopifyCliCommandLine(args)}`,
|
|
12688
|
+
'confirm_required: --confirm deploy',
|
|
12689
|
+
]);
|
|
12829
12690
|
return 0;
|
|
12830
12691
|
}
|
|
12831
|
-
const
|
|
12692
|
+
const dependencies = await prepareShopifySourceDependencies({ dir, runtime });
|
|
12693
|
+
const deployToken = await resolveShopifyCliTokenEnvironment({
|
|
12832
12694
|
command,
|
|
12833
12695
|
sourceArtifactId,
|
|
12834
12696
|
credentials,
|
|
@@ -12836,266 +12698,24 @@ async function runConnectorsShopifySourceDeploy(command, io, runtime) {
|
|
|
12836
12698
|
fetcher,
|
|
12837
12699
|
});
|
|
12838
12700
|
const result = await runShopifyCliCommand({ args, cwd: dir, runtime, env: deployToken.env, redactions: deployToken.redactions });
|
|
12839
|
-
printConnectorsPayload(command, io, {
|
|
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 {
|
|
12701
|
+
printConnectorsPayload(command, io, {
|
|
12939
12702
|
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
12703
|
command: command.command,
|
|
12997
|
-
runtimeArtifactId,
|
|
12998
|
-
selectedConnectorId: resolvedConnector.connectorId,
|
|
12999
|
-
};
|
|
13000
|
-
if (command.json) {
|
|
13001
|
-
io.stdout(JSON.stringify(payload, null, 2));
|
|
13002
|
-
}
|
|
13003
|
-
else {
|
|
13004
|
-
io.stdout(shopifyWorkspaceTextLines(command, payload).join('\n'));
|
|
13005
|
-
}
|
|
13006
|
-
return shopifyWorkspaceCommandSucceeded(command, payload) ? 0 : 1;
|
|
13007
|
-
}
|
|
13008
|
-
function shopifyWorkspaceRequestSucceeded(result) {
|
|
13009
|
-
return result.status >= 200 && result.status < 300 && result.payload.ok !== false;
|
|
13010
|
-
}
|
|
13011
|
-
function shopifyWorkspaceConnectorListTextLines(payload) {
|
|
13012
|
-
const connectors = readShopifyWorkspaceConnectorRecords(payload);
|
|
13013
|
-
const lines = [
|
|
13014
|
-
`runtime_artifact_id: ${String(payload.runtimeArtifactId ?? '')}`,
|
|
13015
|
-
`shop_domain: ${String(payload.shopDomain ?? '')}`,
|
|
13016
|
-
`connectors: ${connectors.length}`,
|
|
13017
|
-
];
|
|
13018
|
-
for (const connector of connectors) {
|
|
13019
|
-
lines.push([
|
|
13020
|
-
String(connector.connectorId ?? ''),
|
|
13021
|
-
String(connector.label ?? connector.appName ?? ''),
|
|
13022
|
-
connector.linked ? 'linked' : 'not_linked',
|
|
13023
|
-
connector.ready ? 'ready' : 'needs_setup',
|
|
13024
|
-
]
|
|
13025
|
-
.filter(Boolean)
|
|
13026
|
-
.join(' | '));
|
|
13027
|
-
}
|
|
13028
|
-
if (typeof payload.reasonCode === 'string') {
|
|
13029
|
-
lines.push(`reason: ${payload.reasonCode}`);
|
|
13030
|
-
}
|
|
13031
|
-
return lines.filter((line) => !line.endsWith(': '));
|
|
13032
|
-
}
|
|
13033
|
-
function shopifyWorkspaceLinkRuntimeTextLines(payload) {
|
|
13034
|
-
const connector = payload.connector && typeof payload.connector === 'object' && !Array.isArray(payload.connector)
|
|
13035
|
-
? payload.connector
|
|
13036
|
-
: {};
|
|
13037
|
-
const source = payload.source && typeof payload.source === 'object' && !Array.isArray(payload.source)
|
|
13038
|
-
? payload.source
|
|
13039
|
-
: {};
|
|
13040
|
-
const workspace = payload.workspace && typeof payload.workspace === 'object' && !Array.isArray(payload.workspace)
|
|
13041
|
-
? payload.workspace
|
|
13042
|
-
: {};
|
|
13043
|
-
return [
|
|
13044
|
-
`link: ${payload.ok === false ? 'failed' : 'created'}`,
|
|
13045
|
-
`runtime_artifact_id: ${String(payload.runtimeArtifactId ?? '')}`,
|
|
13046
|
-
`connector_id: ${String(connector.connectorId ?? '')}`,
|
|
13047
|
-
`source_artifact_id: ${String(source.sourceArtifactId ?? '')}`,
|
|
13048
|
-
workspace.reloadRequired ? 'next: reload the hosted workspace to mount /workspace/shopify-connector' : '',
|
|
13049
|
-
...(payload.reasonCode ? [`reason: ${String(payload.reasonCode)}`] : []),
|
|
13050
|
-
].filter(Boolean);
|
|
13051
|
-
}
|
|
13052
|
-
async function runShopifyWorkspaceConnectorsList(command, io, runtime) {
|
|
13053
|
-
const { baseUrl, runtimeArtifactId } = readShopifyWorkspaceRuntimeIdentity(runtime);
|
|
13054
|
-
const result = await requestShopifyWorkspaceConnectorsList({
|
|
13055
|
-
baseUrl,
|
|
13056
|
-
runtimeArtifactId,
|
|
13057
|
-
fetcher: runtime.fetcher ?? fetch,
|
|
13058
|
-
});
|
|
13059
|
-
const payload = {
|
|
13060
|
-
...result.payload,
|
|
13061
|
-
command: command.command,
|
|
13062
|
-
runtimeArtifactId,
|
|
13063
|
-
};
|
|
13064
|
-
if (command.json) {
|
|
13065
|
-
io.stdout(JSON.stringify(payload, null, 2));
|
|
13066
|
-
}
|
|
13067
|
-
else {
|
|
13068
|
-
io.stdout(shopifyWorkspaceConnectorListTextLines(payload).join('\n'));
|
|
13069
|
-
}
|
|
13070
|
-
return shopifyWorkspaceRequestSucceeded(result) ? 0 : 1;
|
|
13071
|
-
}
|
|
13072
|
-
async function runShopifyWorkspaceLinkRuntime(command, io, runtime) {
|
|
13073
|
-
const { baseUrl, runtimeArtifactId } = readShopifyWorkspaceRuntimeIdentity(runtime);
|
|
13074
|
-
const result = await requestShopifyWorkspaceLinkRuntime({
|
|
13075
|
-
baseUrl,
|
|
13076
|
-
runtimeArtifactId,
|
|
13077
|
-
connectorId: command.connectorId,
|
|
13078
|
-
fetcher: runtime.fetcher ?? fetch,
|
|
13079
|
-
});
|
|
13080
|
-
if (shopifyWorkspaceRequestSucceeded(result)) {
|
|
13081
|
-
await writeHostedWorkspaceConnectorSelection({
|
|
13082
|
-
runtimeArtifactId,
|
|
13083
|
-
connectorId: command.connectorId,
|
|
13084
|
-
}, runtime);
|
|
13085
|
-
}
|
|
13086
|
-
const payload = {
|
|
13087
|
-
...result.payload,
|
|
13088
|
-
command: command.command,
|
|
13089
|
-
runtimeArtifactId,
|
|
13090
12704
|
connectorId: command.connectorId,
|
|
13091
|
-
|
|
13092
|
-
|
|
13093
|
-
|
|
13094
|
-
|
|
13095
|
-
|
|
13096
|
-
|
|
13097
|
-
}
|
|
13098
|
-
|
|
12705
|
+
sourceArtifactId,
|
|
12706
|
+
dir,
|
|
12707
|
+
exitCode: result.exitCode,
|
|
12708
|
+
stdout: result.stdout,
|
|
12709
|
+
stderr: result.stderr,
|
|
12710
|
+
dependencies,
|
|
12711
|
+
}, [
|
|
12712
|
+
`connector_id: ${command.connectorId}`,
|
|
12713
|
+
`source_artifact_id: ${sourceArtifactId}`,
|
|
12714
|
+
`source_dir: ${dir}`,
|
|
12715
|
+
`dependencies: ${dependencies.status === 'ready' ? (dependencies.installed ? 'installed' : 'cached') : 'not_required'}`,
|
|
12716
|
+
'deploy: completed',
|
|
12717
|
+
]);
|
|
12718
|
+
return 0;
|
|
13099
12719
|
}
|
|
13100
12720
|
async function runConnectorsShopifyDelete(command, io, runtime) {
|
|
13101
12721
|
if (command.confirm !== 'delete') {
|
|
@@ -13109,7 +12729,10 @@ async function runConnectorsShopifyDelete(command, io, runtime) {
|
|
|
13109
12729
|
token: credentials.token,
|
|
13110
12730
|
fetcher: runtime.fetcher ?? fetch,
|
|
13111
12731
|
});
|
|
13112
|
-
printConnectorsPayload(command, io, { ...payload, connectorId: command.connectorId }, [
|
|
12732
|
+
printConnectorsPayload(command, io, { ...payload, connectorId: command.connectorId }, [
|
|
12733
|
+
`connector_id: ${command.connectorId}`,
|
|
12734
|
+
'status: deleted',
|
|
12735
|
+
]);
|
|
13113
12736
|
return 0;
|
|
13114
12737
|
}
|
|
13115
12738
|
function runConnectorsBind(command, io) {
|
|
@@ -13936,6 +13559,9 @@ function cliErrorMessage(error) {
|
|
|
13936
13559
|
return String(error);
|
|
13937
13560
|
}
|
|
13938
13561
|
function cliErrorCode(error) {
|
|
13562
|
+
if (error instanceof TenderCliDependencyInstallError) {
|
|
13563
|
+
return 'dependency_install_failed';
|
|
13564
|
+
}
|
|
13939
13565
|
if (error instanceof TenderCliChildProcessError) {
|
|
13940
13566
|
return 'child_process_failed';
|
|
13941
13567
|
}
|
|
@@ -14148,14 +13774,6 @@ export async function runTenderCli(args, io = {
|
|
|
14148
13774
|
else if (command.topic === 'connectors bind') {
|
|
14149
13775
|
io.stdout(connectorsBindHelp());
|
|
14150
13776
|
}
|
|
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
13777
|
else if (command.topic === 'auth') {
|
|
14160
13778
|
io.stdout(authHelp());
|
|
14161
13779
|
}
|
|
@@ -14416,15 +14034,6 @@ export async function runTenderCli(args, io = {
|
|
|
14416
14034
|
if (command.command === 'connectors shopify source deploy') {
|
|
14417
14035
|
return await runConnectorsShopifySourceDeploy(command, io, runtime);
|
|
14418
14036
|
}
|
|
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
14037
|
if (command.command === 'connectors shopify delete') {
|
|
14429
14038
|
return await runConnectorsShopifyDelete(command, io, runtime);
|
|
14430
14039
|
}
|
|
@@ -14597,7 +14206,10 @@ export async function runTenderCli(args, io = {
|
|
|
14597
14206
|
}
|
|
14598
14207
|
catch (error) {
|
|
14599
14208
|
if (args.includes('--json') &&
|
|
14600
|
-
((args[0] === 'app' && (args[1] === 'db' || (args[1] === 'agent' && args[2] === 'heartbeat'))) ||
|
|
14209
|
+
((args[0] === 'app' && (args[1] === 'db' || (args[1] === 'agent' && args[2] === 'heartbeat'))) ||
|
|
14210
|
+
args[0] === 'playbooks' ||
|
|
14211
|
+
args[0] === 'connectors' ||
|
|
14212
|
+
args[0] === 'shopify')) {
|
|
14601
14213
|
io.stdout(JSON.stringify(cliErrorPayload(error, args, command), null, 2));
|
|
14602
14214
|
return error instanceof TenderCliUsageError ? 2 : 1;
|
|
14603
14215
|
}
|