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