@tenderprompt/cli 0.1.5 → 0.1.7
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 +102 -2
- package/dist/index.js +419 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,15 +16,41 @@ npm install -g @tenderprompt/cli
|
|
|
16
16
|
Then run:
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
tender --
|
|
19
|
+
tender capabilities --json
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
## What You Can Do
|
|
23
|
+
|
|
24
|
+
Use the CLI to work with Tender Apps from a local checkout or coding-agent
|
|
25
|
+
session:
|
|
26
|
+
|
|
27
|
+
- sign in with a bounded device token and optional artifact scope
|
|
28
|
+
- list, create, and rename apps
|
|
29
|
+
- bootstrap a local app checkout with Tender project context
|
|
30
|
+
- configure the Tender artifact Git remote and credential helper
|
|
31
|
+
- discover the current CLI command surface with `tender capabilities --json`
|
|
32
|
+
- preview outbound HTTP binding files without writing them
|
|
33
|
+
- validate app source locally before pushing
|
|
34
|
+
- build and preview a commit through the Tender API
|
|
35
|
+
- publish only after a dry run or explicit `--confirm publish`
|
|
36
|
+
- inspect artifact-scoped analytics without SQL or platform credentials
|
|
37
|
+
- run server-validated chart specs and export aggregate rows
|
|
38
|
+
- generate starter analytics suggestions
|
|
39
|
+
- create saved analytics dashboards and charts, with `--dry-run` available for
|
|
40
|
+
write previews
|
|
41
|
+
|
|
42
|
+
The CLI is deterministic by design: every workflow has a non-interactive flag
|
|
43
|
+
path, supports `--json` where automation needs structured output, and avoids
|
|
44
|
+
interactive prompts unless the command is explicitly an auth flow.
|
|
45
|
+
|
|
22
46
|
## Typical Flow
|
|
23
47
|
|
|
24
48
|
```bash
|
|
49
|
+
tender capabilities --json
|
|
50
|
+
|
|
25
51
|
tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d
|
|
26
52
|
|
|
27
|
-
tender app
|
|
53
|
+
tender app init artifact_123 --dir ./widget --json
|
|
28
54
|
|
|
29
55
|
cd ./widget
|
|
30
56
|
npm run check
|
|
@@ -35,6 +61,16 @@ tender app preview artifact_123 --commit "$(git rev-parse HEAD)" --stream --json
|
|
|
35
61
|
tender app publish artifact_123 --commit "$(git rev-parse HEAD)" --confirm publish --stream --json
|
|
36
62
|
```
|
|
37
63
|
|
|
64
|
+
For agents, keep the pattern simple:
|
|
65
|
+
|
|
66
|
+
1. start with `tender capabilities --json`
|
|
67
|
+
2. use `tender app init` to clone/bootstrap an existing app checkout
|
|
68
|
+
3. inspect and edit files locally
|
|
69
|
+
4. run `tender app doctor` and local project checks
|
|
70
|
+
5. push source with Git
|
|
71
|
+
6. call `preview`, `publish --dry-run`, then `publish --confirm publish` only
|
|
72
|
+
when the user asked to publish
|
|
73
|
+
|
|
38
74
|
## Auth
|
|
39
75
|
|
|
40
76
|
Use the device login flow to mint a local token:
|
|
@@ -71,10 +107,12 @@ indexing the accepted commit after upstream Git accepts the push.
|
|
|
71
107
|
## Useful Commands
|
|
72
108
|
|
|
73
109
|
```bash
|
|
110
|
+
tender capabilities --json
|
|
74
111
|
tender app list --json
|
|
75
112
|
tender app create --name "Exit Intent Widget" --json
|
|
76
113
|
tender app update artifact_123 --name "Sale Widget" --json
|
|
77
114
|
tender app init artifact_123 --dir ./widget --json
|
|
115
|
+
tender app bindings outbound --id shopify --host admin.shopify.com --dry-run --json
|
|
78
116
|
tender app context status artifact_123 --dir ./widget --json
|
|
79
117
|
tender app doctor --dir ./widget --json
|
|
80
118
|
tender app validate artifact_123 --json
|
|
@@ -83,6 +121,64 @@ tender app preview artifact_123 --commit "$(git rev-parse HEAD)" --stream --json
|
|
|
83
121
|
tender app publish artifact_123 --dry-run --json
|
|
84
122
|
```
|
|
85
123
|
|
|
124
|
+
## Analytics
|
|
125
|
+
|
|
126
|
+
Analytics commands are artifact-scoped and go through Tender's server-side
|
|
127
|
+
validation. The CLI never exposes raw query credentials, raw SQL, or cross-app
|
|
128
|
+
data.
|
|
129
|
+
|
|
130
|
+
Start with capabilities. This is the self-documenting entrypoint for coding
|
|
131
|
+
agents:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
tender app analytics capabilities artifact_123 --include-catalog --range 30d --json
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
That response tells the agent:
|
|
138
|
+
|
|
139
|
+
- which analytics commands are supported
|
|
140
|
+
- the safe chart-spec shape
|
|
141
|
+
- retention, range, quota, and security limits
|
|
142
|
+
- existing saved dashboards and charts
|
|
143
|
+
- bounded observed event names, request paths, property keys, rough types, and
|
|
144
|
+
counts from artifact-scoped catalog samples
|
|
145
|
+
|
|
146
|
+
Common read-only commands:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
tender app analytics summary artifact_123 --range 30d --json
|
|
150
|
+
|
|
151
|
+
tender app analytics query artifact_123 --spec chart.json --json
|
|
152
|
+
|
|
153
|
+
tender app analytics query artifact_123 --spec - --json < chart.json
|
|
154
|
+
|
|
155
|
+
tender app analytics export catalog artifact_123 --range 30d --json
|
|
156
|
+
|
|
157
|
+
tender app analytics export query artifact_123 --spec chart.json --format csv
|
|
158
|
+
|
|
159
|
+
tender app analytics suggestions artifact_123 --range 30d --json
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Safe write previews:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
tender app analytics dashboards artifact_123 --create "Agent dashboard" --dry-run --json
|
|
166
|
+
|
|
167
|
+
tender app analytics charts create artifact_123 \
|
|
168
|
+
--dashboard analytics_dashboard_123 \
|
|
169
|
+
--title "Signup funnel" \
|
|
170
|
+
--spec funnel.json \
|
|
171
|
+
--dry-run \
|
|
172
|
+
--json
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Real saved-dashboard writes use the same commands without `--dry-run`.
|
|
176
|
+
|
|
177
|
+
Agents should combine `capabilities --include-catalog` with local source-code
|
|
178
|
+
inspection. For example, inspect where the app calls
|
|
179
|
+
`__TP_ANALYTICS.invoke(...)`, propose a chart spec, validate it with
|
|
180
|
+
`analytics query`, and only then create a saved chart.
|
|
181
|
+
|
|
86
182
|
## Safety Model
|
|
87
183
|
|
|
88
184
|
- Local agents run in the user's environment, not in Tender.
|
|
@@ -93,6 +189,10 @@ tender app publish artifact_123 --dry-run --json
|
|
|
93
189
|
- `tender app publish` requires `--dry-run` or `--confirm publish`.
|
|
94
190
|
- Tokens are stored outside the project checkout and are not written into Git
|
|
95
191
|
remote URLs.
|
|
192
|
+
- Analytics commands use tenant/artifact-scoped API routes. They do not expose
|
|
193
|
+
raw query credentials or raw SQL.
|
|
194
|
+
- Raw analytics event export is intentionally not available; exports are bounded
|
|
195
|
+
catalog metadata or validated aggregate query rows.
|
|
96
196
|
|
|
97
197
|
## Related Package
|
|
98
198
|
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,8 @@ function rootHelp() {
|
|
|
20
20
|
return `Usage: tender <command> [options]
|
|
21
21
|
|
|
22
22
|
Commands:
|
|
23
|
+
tender capabilities --json
|
|
24
|
+
Describe the CLI command surface for coding agents
|
|
23
25
|
tender auth login --device
|
|
24
26
|
Authorize this machine with a browser-approved device flow
|
|
25
27
|
tender auth status Show whether a Tender API token is available
|
|
@@ -58,6 +60,8 @@ Commands:
|
|
|
58
60
|
Report whether managed context is stale
|
|
59
61
|
tender app context refresh <app-id>
|
|
60
62
|
Refresh managed context files
|
|
63
|
+
tender app bindings outbound --id <id> --host <host> --dry-run
|
|
64
|
+
Preview files for an outbound HTTP binding
|
|
61
65
|
tender app git setup <app-id>
|
|
62
66
|
Configure a checkout to push to the app remote
|
|
63
67
|
tender app doctor Validate a local Tender App checkout
|
|
@@ -68,6 +72,7 @@ Compatibility:
|
|
|
68
72
|
tender artifacts ... Legacy alias for Tender App commands
|
|
69
73
|
|
|
70
74
|
Examples:
|
|
75
|
+
tender capabilities --json
|
|
71
76
|
tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d
|
|
72
77
|
tender auth status --json
|
|
73
78
|
tender app list --json
|
|
@@ -86,6 +91,7 @@ Examples:
|
|
|
86
91
|
tender app context fetch artifact_123 --dir ./widget --dry-run --json
|
|
87
92
|
tender app context status artifact_123 --dir ./widget --json
|
|
88
93
|
tender app context refresh artifact_123 --dir ./widget --dry-run --json
|
|
94
|
+
tender app bindings outbound --id shopify --host admin.shopify.com --dry-run --json
|
|
89
95
|
tender app git setup artifact_123 --dir ./widget --json
|
|
90
96
|
tender app doctor --dir ./widget
|
|
91
97
|
tender app generate-env-types --dir ./widget --json
|
|
@@ -175,6 +181,8 @@ Commands:
|
|
|
175
181
|
Show managed context freshness for a checkout
|
|
176
182
|
tender app context refresh <app-id>
|
|
177
183
|
Refresh managed context files in a checkout
|
|
184
|
+
tender app bindings outbound --id <id> --host <host> --dry-run
|
|
185
|
+
Preview files for an outbound HTTP binding
|
|
178
186
|
tender app git setup <app-id>
|
|
179
187
|
Configure the tender Git remote and credential helper
|
|
180
188
|
tender app doctor Validate local context, TypeScript scaffold, and Tender App rules
|
|
@@ -205,6 +213,7 @@ Examples:
|
|
|
205
213
|
tender app context fetch artifact_123 --dir ./widget --dry-run --json
|
|
206
214
|
tender app context status artifact_123 --dir ./widget --json
|
|
207
215
|
tender app context refresh artifact_123 --dir ./widget --dry-run --json
|
|
216
|
+
tender app bindings outbound --id shopify --host admin.shopify.com --dry-run --json
|
|
208
217
|
tender app git setup artifact_123 --dir ./widget --json
|
|
209
218
|
tender app doctor --dir ./widget
|
|
210
219
|
tender app generate-env-types --dir ./widget --json
|
|
@@ -217,6 +226,15 @@ function artifactsHelp() {
|
|
|
217
226
|
Legacy alias:
|
|
218
227
|
tender artifacts ... continues to work for existing automation. Prefer tender app ... for new workflows.`;
|
|
219
228
|
}
|
|
229
|
+
function capabilitiesHelp() {
|
|
230
|
+
return `Usage: tender capabilities [--json]
|
|
231
|
+
|
|
232
|
+
Describes the Tender CLI surface for coding agents. Use this first instead of
|
|
233
|
+
copying long command lists into skill prompts.
|
|
234
|
+
|
|
235
|
+
Examples:
|
|
236
|
+
tender capabilities --json`;
|
|
237
|
+
}
|
|
220
238
|
function analyticsHelp() {
|
|
221
239
|
return `Usage: tender app analytics <command> <app-id> [options]
|
|
222
240
|
|
|
@@ -441,6 +459,26 @@ Examples:
|
|
|
441
459
|
tender app generate-env-types --dir ./widget
|
|
442
460
|
tender app generate-env-types --dir ./widget --json`;
|
|
443
461
|
}
|
|
462
|
+
function bindingsOutboundHelp() {
|
|
463
|
+
return `Usage: tender app bindings outbound --id <id> --host <host> --dry-run [--name <ENV_NAME>] [--label <label>] [--interface <TypeName>] [--json]
|
|
464
|
+
|
|
465
|
+
Preview the file shape for an outbound HTTP binding. App server code cannot call
|
|
466
|
+
third-party internet URLs directly; external APIs, OAuth/token exchanges,
|
|
467
|
+
webhooks, and secrets must go through bindings.
|
|
468
|
+
|
|
469
|
+
Options:
|
|
470
|
+
--id <id> Binding id and directory name, for example shopify-api.
|
|
471
|
+
--host <host> Explicit lowercase host without protocol, wildcard, path, query, or port.
|
|
472
|
+
--name <ENV_NAME> App env binding name. Defaults to the id converted to UPPER_SNAKE.
|
|
473
|
+
--label <label> Human label for app.json. Defaults to a title-cased id.
|
|
474
|
+
--interface <TypeName> TypeScript interface and worker class name. Defaults to a PascalCase id plus Binding.
|
|
475
|
+
--dry-run Required. Prints files and app.json binding entry without writing.
|
|
476
|
+
--json Emit stable JSON for coding agents.
|
|
477
|
+
|
|
478
|
+
Examples:
|
|
479
|
+
tender app bindings outbound --id shopify --host admin.shopify.com --dry-run --json
|
|
480
|
+
tender app bindings outbound --id yotpo-api --host api.yotpo.com --name YOTPO --label "Yotpo API" --dry-run --json`;
|
|
481
|
+
}
|
|
444
482
|
function gitRemoteHelp() {
|
|
445
483
|
return `Usage: tender app git remote <app-id> [--base-url <url>] [--token <token>] [--profile <name>] [--ttl <7d|14d|30d>] [--json]
|
|
446
484
|
|
|
@@ -605,6 +643,149 @@ Options:
|
|
|
605
643
|
Examples:
|
|
606
644
|
tender app publish watch artifact_123 --job job_123 --stream --json`;
|
|
607
645
|
}
|
|
646
|
+
function tenderCliCapabilities() {
|
|
647
|
+
return {
|
|
648
|
+
ok: true,
|
|
649
|
+
surface: "tender_cli",
|
|
650
|
+
description: "Agent-safe Tender App source, lifecycle, analytics, and local scaffold controls.",
|
|
651
|
+
discovery: {
|
|
652
|
+
primary: "tender capabilities --json",
|
|
653
|
+
layeredHelp: [
|
|
654
|
+
"tender --help",
|
|
655
|
+
"tender auth --help",
|
|
656
|
+
"tender app --help",
|
|
657
|
+
"tender app analytics --help",
|
|
658
|
+
"tender app bindings outbound --help",
|
|
659
|
+
],
|
|
660
|
+
},
|
|
661
|
+
workflows: [
|
|
662
|
+
{
|
|
663
|
+
name: "authenticate",
|
|
664
|
+
when: "Before reading or changing remote Tender App records.",
|
|
665
|
+
commands: [
|
|
666
|
+
"tender auth status --json",
|
|
667
|
+
"tender auth login --device --profile edit-preview --artifact <artifact-id> --ttl 7d --json",
|
|
668
|
+
],
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
name: "clone_or_bootstrap_existing_app",
|
|
672
|
+
when: "When the user wants to download or work on an existing app locally.",
|
|
673
|
+
commands: [
|
|
674
|
+
"tender app init <artifact-id> --dir <dir> --dry-run --json",
|
|
675
|
+
"tender app init <artifact-id> --dir <dir> --json",
|
|
676
|
+
],
|
|
677
|
+
notes: [
|
|
678
|
+
"init is the normal clone/bootstrap command.",
|
|
679
|
+
"If the app already has artifact Git source, init fetches it and configures the tender remote.",
|
|
680
|
+
"If no artifact Git source exists yet, init writes managed context/scaffold files and configures the tender remote.",
|
|
681
|
+
"Use context fetch only when you already have a checkout and only need AGENTS.md, skills, or Tender context refreshed.",
|
|
682
|
+
],
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
name: "local_edit_preview_publish",
|
|
686
|
+
when: "When editing app source from a local checkout.",
|
|
687
|
+
commands: [
|
|
688
|
+
"tender app doctor --dir <dir> --json",
|
|
689
|
+
"tender app git setup <artifact-id> --dir <dir> --dry-run --json",
|
|
690
|
+
"git push tender main",
|
|
691
|
+
"tender app preview <artifact-id> --commit $(git rev-parse HEAD) --stream --json",
|
|
692
|
+
"tender app publish <artifact-id> --dry-run --json",
|
|
693
|
+
"tender app publish <artifact-id> --commit $(git rev-parse HEAD) --confirm publish --stream --json",
|
|
694
|
+
],
|
|
695
|
+
notes: [
|
|
696
|
+
"Publishing is production-facing and requires explicit user intent.",
|
|
697
|
+
],
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
name: "analytics_agent_loop",
|
|
701
|
+
when: "When understanding app analytics or creating saved charts.",
|
|
702
|
+
commands: [
|
|
703
|
+
"tender app analytics capabilities <artifact-id> --include-catalog --range 30d --json",
|
|
704
|
+
"tender app analytics query <artifact-id> --spec chart.json --json",
|
|
705
|
+
"tender app analytics export query <artifact-id> --spec chart.json --format csv",
|
|
706
|
+
"tender app analytics dashboards <artifact-id> --create \"Agent dashboard\" --dry-run --json",
|
|
707
|
+
"tender app analytics charts create <artifact-id> --dashboard <dashboard-id> --title <title> --spec chart.json --dry-run --json",
|
|
708
|
+
],
|
|
709
|
+
notes: [
|
|
710
|
+
"Combine CLI capabilities with local source inspection of analytics calls.",
|
|
711
|
+
"Validate chart specs with query before saving charts.",
|
|
712
|
+
],
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
name: "outbound_http_binding",
|
|
716
|
+
when: "When app code needs external APIs, OAuth/token exchange, webhooks, provider SDKs, or secrets.",
|
|
717
|
+
commands: [
|
|
718
|
+
"tender app bindings outbound --id <id> --host <host> --dry-run --json",
|
|
719
|
+
"tender app generate-env-types --dir <dir> --json",
|
|
720
|
+
"tender app doctor --dir <dir> --json",
|
|
721
|
+
],
|
|
722
|
+
notes: [
|
|
723
|
+
"src/server.ts cannot call third-party internet URLs directly.",
|
|
724
|
+
"Outbound calls are blocked unless they go through a binding with outbound:http and explicit allowedHosts.",
|
|
725
|
+
],
|
|
726
|
+
},
|
|
727
|
+
],
|
|
728
|
+
commands: [
|
|
729
|
+
{
|
|
730
|
+
command: "tender capabilities --json",
|
|
731
|
+
purpose: "Discover this CLI surface in a machine-readable shape.",
|
|
732
|
+
requiresAuth: false,
|
|
733
|
+
writes: false,
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
command: "tender app init <artifact-id> --dir <dir> --json",
|
|
737
|
+
purpose: "Clone/bootstrap an existing Tender App checkout and configure context plus the tender Git remote.",
|
|
738
|
+
requiresAuth: true,
|
|
739
|
+
writes: true,
|
|
740
|
+
dryRun: true,
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
command: "tender app context fetch <artifact-id> --dir <dir> --json",
|
|
744
|
+
purpose: "Fetch managed AGENTS.md, skills, and Tender context into an existing checkout.",
|
|
745
|
+
requiresAuth: true,
|
|
746
|
+
writes: true,
|
|
747
|
+
dryRun: true,
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
command: "tender app bindings outbound --id <id> --host <host> --dry-run --json",
|
|
751
|
+
purpose: "Preview app.json and binding files for an outbound HTTP integration.",
|
|
752
|
+
requiresAuth: false,
|
|
753
|
+
writes: false,
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
command: "tender app analytics capabilities <artifact-id> --include-catalog --range 30d --json",
|
|
757
|
+
purpose: "Discover artifact-scoped analytics commands, chart specs, saved dashboards, and bounded observed catalog.",
|
|
758
|
+
requiresAuth: true,
|
|
759
|
+
writes: false,
|
|
760
|
+
},
|
|
761
|
+
],
|
|
762
|
+
security: [
|
|
763
|
+
"Prefer artifact-scoped device tokens.",
|
|
764
|
+
"Do not write Tender tokens into the project checkout.",
|
|
765
|
+
"Do not call Tender HTTP APIs directly from agent workflows.",
|
|
766
|
+
"Do not put third-party fetch calls in src/server.ts; use outbound bindings.",
|
|
767
|
+
"Do not request direct query credentials or raw SQL for analytics.",
|
|
768
|
+
],
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
function runCapabilities(command, io) {
|
|
772
|
+
const payload = tenderCliCapabilities();
|
|
773
|
+
if (command.json) {
|
|
774
|
+
io.stdout(JSON.stringify(payload, null, 2));
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
io.stdout([
|
|
778
|
+
"Tender CLI capabilities",
|
|
779
|
+
"",
|
|
780
|
+
"Start with:",
|
|
781
|
+
" tender capabilities --json",
|
|
782
|
+
"",
|
|
783
|
+
"Useful next help topics:",
|
|
784
|
+
...payload.discovery.layeredHelp.map((topic) => ` ${topic}`),
|
|
785
|
+
].join("\n"));
|
|
786
|
+
}
|
|
787
|
+
return 0;
|
|
788
|
+
}
|
|
608
789
|
function normalizeBaseUrl(value) {
|
|
609
790
|
return (value ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
610
791
|
}
|
|
@@ -2071,6 +2252,127 @@ function parseRequiredFlagValue(value, flag) {
|
|
|
2071
2252
|
}
|
|
2072
2253
|
return value;
|
|
2073
2254
|
}
|
|
2255
|
+
function parseCapabilitiesArgs(args) {
|
|
2256
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
2257
|
+
return { command: "help", topic: "capabilities" };
|
|
2258
|
+
}
|
|
2259
|
+
let json = false;
|
|
2260
|
+
for (const arg of args) {
|
|
2261
|
+
if (arg === "--json") {
|
|
2262
|
+
json = true;
|
|
2263
|
+
continue;
|
|
2264
|
+
}
|
|
2265
|
+
throw new TenderCliUsageError(`Unknown capabilities option: ${arg}`);
|
|
2266
|
+
}
|
|
2267
|
+
return { command: "capabilities", json };
|
|
2268
|
+
}
|
|
2269
|
+
function parseBindingId(value) {
|
|
2270
|
+
if (!value || value.startsWith("-")) {
|
|
2271
|
+
throw new TenderCliUsageError("--id requires a binding id.");
|
|
2272
|
+
}
|
|
2273
|
+
if (!/^[a-z][a-z0-9-]*$/.test(value)) {
|
|
2274
|
+
throw new TenderCliUsageError("--id must start with a lowercase letter and contain only lowercase letters, numbers, and dashes. Example: shopify-api");
|
|
2275
|
+
}
|
|
2276
|
+
return value;
|
|
2277
|
+
}
|
|
2278
|
+
function parseAllowedHost(value) {
|
|
2279
|
+
if (!value || value.startsWith("-")) {
|
|
2280
|
+
throw new TenderCliUsageError("--host requires a hostname.");
|
|
2281
|
+
}
|
|
2282
|
+
if (value.length === 0 ||
|
|
2283
|
+
value.includes("/") ||
|
|
2284
|
+
value.includes(":") ||
|
|
2285
|
+
value.includes("?") ||
|
|
2286
|
+
value.includes("#") ||
|
|
2287
|
+
value.includes("*") ||
|
|
2288
|
+
value !== value.toLowerCase()) {
|
|
2289
|
+
throw new TenderCliUsageError("--host must be an explicit lowercase hostname without protocol, wildcard, path, query, or port. Example: api.example.com");
|
|
2290
|
+
}
|
|
2291
|
+
return value;
|
|
2292
|
+
}
|
|
2293
|
+
function parseEnvBindingName(value) {
|
|
2294
|
+
if (!value || value.startsWith("-")) {
|
|
2295
|
+
throw new TenderCliUsageError("--name requires an env binding name.");
|
|
2296
|
+
}
|
|
2297
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(value)) {
|
|
2298
|
+
throw new TenderCliUsageError("--name must be UPPER_SNAKE_CASE. Example: SHOPIFY_API");
|
|
2299
|
+
}
|
|
2300
|
+
return value;
|
|
2301
|
+
}
|
|
2302
|
+
function parseTypeName(value, flag) {
|
|
2303
|
+
if (!value || value.startsWith("-")) {
|
|
2304
|
+
throw new TenderCliUsageError(`${flag} requires a TypeScript type name.`);
|
|
2305
|
+
}
|
|
2306
|
+
if (!/^[A-Z][A-Za-z0-9]*$/.test(value)) {
|
|
2307
|
+
throw new TenderCliUsageError(`${flag} must be a PascalCase TypeScript type name. Example: ShopifyApiBinding`);
|
|
2308
|
+
}
|
|
2309
|
+
return value;
|
|
2310
|
+
}
|
|
2311
|
+
function parseBindingsOutboundArgs(args) {
|
|
2312
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
2313
|
+
return { command: "help", topic: "artifacts bindings outbound" };
|
|
2314
|
+
}
|
|
2315
|
+
let id = null;
|
|
2316
|
+
let host = null;
|
|
2317
|
+
let name = null;
|
|
2318
|
+
let label = null;
|
|
2319
|
+
let interfaceName = null;
|
|
2320
|
+
let dryRun = false;
|
|
2321
|
+
let json = false;
|
|
2322
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2323
|
+
const arg = args[index];
|
|
2324
|
+
if (arg === "--json") {
|
|
2325
|
+
json = true;
|
|
2326
|
+
continue;
|
|
2327
|
+
}
|
|
2328
|
+
if (arg === "--dry-run") {
|
|
2329
|
+
dryRun = true;
|
|
2330
|
+
continue;
|
|
2331
|
+
}
|
|
2332
|
+
if (arg === "--id") {
|
|
2333
|
+
id = parseBindingId(args[index + 1]);
|
|
2334
|
+
index += 1;
|
|
2335
|
+
continue;
|
|
2336
|
+
}
|
|
2337
|
+
if (arg === "--host") {
|
|
2338
|
+
host = parseAllowedHost(args[index + 1]);
|
|
2339
|
+
index += 1;
|
|
2340
|
+
continue;
|
|
2341
|
+
}
|
|
2342
|
+
if (arg === "--name") {
|
|
2343
|
+
name = parseEnvBindingName(args[index + 1]);
|
|
2344
|
+
index += 1;
|
|
2345
|
+
continue;
|
|
2346
|
+
}
|
|
2347
|
+
if (arg === "--label") {
|
|
2348
|
+
label = parseRequiredFlagValue(args[index + 1], "--label");
|
|
2349
|
+
index += 1;
|
|
2350
|
+
continue;
|
|
2351
|
+
}
|
|
2352
|
+
if (arg === "--interface") {
|
|
2353
|
+
interfaceName = parseTypeName(args[index + 1], "--interface");
|
|
2354
|
+
index += 1;
|
|
2355
|
+
continue;
|
|
2356
|
+
}
|
|
2357
|
+
throw new TenderCliUsageError(`Unknown bindings outbound option: ${arg}`);
|
|
2358
|
+
}
|
|
2359
|
+
if (!id || !host) {
|
|
2360
|
+
throw new TenderCliUsageError("--id and --host are required. Example: tender app bindings outbound --id shopify --host admin.shopify.com --dry-run --json");
|
|
2361
|
+
}
|
|
2362
|
+
if (!dryRun) {
|
|
2363
|
+
throw new TenderCliUsageError("bindings outbound currently requires --dry-run. Example: tender app bindings outbound --id shopify --host admin.shopify.com --dry-run --json");
|
|
2364
|
+
}
|
|
2365
|
+
return {
|
|
2366
|
+
command: "artifacts bindings outbound",
|
|
2367
|
+
id,
|
|
2368
|
+
host,
|
|
2369
|
+
name,
|
|
2370
|
+
label,
|
|
2371
|
+
interfaceName,
|
|
2372
|
+
dryRun,
|
|
2373
|
+
json,
|
|
2374
|
+
};
|
|
2375
|
+
}
|
|
2074
2376
|
function parseAnalyticsCommonArgs(args, startIndex) {
|
|
2075
2377
|
let baseUrl = null;
|
|
2076
2378
|
let flagToken = null;
|
|
@@ -2359,6 +2661,9 @@ function parseTenderArgs(args) {
|
|
|
2359
2661
|
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
2360
2662
|
return { command: "help", topic: "root" };
|
|
2361
2663
|
}
|
|
2664
|
+
if (args[0] === "capabilities") {
|
|
2665
|
+
return parseCapabilitiesArgs(args.slice(1));
|
|
2666
|
+
}
|
|
2362
2667
|
if (args[0] === "auth") {
|
|
2363
2668
|
if (args[1] === undefined || args[1] === "--help" || args[1] === "-h") {
|
|
2364
2669
|
return { command: "help", topic: "auth" };
|
|
@@ -2406,6 +2711,15 @@ function parseTenderArgs(args) {
|
|
|
2406
2711
|
if (resourceArgs[1] === "context" && resourceArgs[2] === "refresh") {
|
|
2407
2712
|
return parseContextRefreshArgs(resourceArgs.slice(3));
|
|
2408
2713
|
}
|
|
2714
|
+
if (resourceArgs[1] === "bindings" && resourceArgs[2] === "outbound") {
|
|
2715
|
+
return parseBindingsOutboundArgs(resourceArgs.slice(3));
|
|
2716
|
+
}
|
|
2717
|
+
if (resourceArgs[1] === "bindings" &&
|
|
2718
|
+
(resourceArgs[2] === undefined ||
|
|
2719
|
+
resourceArgs[2] === "--help" ||
|
|
2720
|
+
resourceArgs[2] === "-h")) {
|
|
2721
|
+
return { command: "help", topic: "artifacts bindings outbound" };
|
|
2722
|
+
}
|
|
2409
2723
|
if (resourceArgs[1] === "analytics" &&
|
|
2410
2724
|
(resourceArgs[2] === undefined ||
|
|
2411
2725
|
resourceArgs[2] === "--help" ||
|
|
@@ -2659,6 +2973,98 @@ async function runGenerateEnvTypes(command, io) {
|
|
|
2659
2973
|
}
|
|
2660
2974
|
return 0;
|
|
2661
2975
|
}
|
|
2976
|
+
function titleCaseBindingId(id) {
|
|
2977
|
+
return id
|
|
2978
|
+
.split("-")
|
|
2979
|
+
.filter(Boolean)
|
|
2980
|
+
.map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
|
|
2981
|
+
.join(" ");
|
|
2982
|
+
}
|
|
2983
|
+
function pascalCaseBindingId(id) {
|
|
2984
|
+
return id
|
|
2985
|
+
.split("-")
|
|
2986
|
+
.filter(Boolean)
|
|
2987
|
+
.map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
|
|
2988
|
+
.join("");
|
|
2989
|
+
}
|
|
2990
|
+
function envNameFromBindingId(id) {
|
|
2991
|
+
return id.toUpperCase().replaceAll("-", "_");
|
|
2992
|
+
}
|
|
2993
|
+
function createOutboundBindingShape(command) {
|
|
2994
|
+
const interfaceName = command.interfaceName ?? `${pascalCaseBindingId(command.id)}Binding`;
|
|
2995
|
+
const label = command.label ?? titleCaseBindingId(command.id);
|
|
2996
|
+
const name = command.name ?? envNameFromBindingId(command.id);
|
|
2997
|
+
const bindingDirectory = `bindings/${command.id}`;
|
|
2998
|
+
const bindingJson = {
|
|
2999
|
+
id: command.id,
|
|
3000
|
+
entry: "src/worker.ts",
|
|
3001
|
+
entrypoint: interfaceName,
|
|
3002
|
+
interface: interfaceName,
|
|
3003
|
+
permissions: ["outbound:http"],
|
|
3004
|
+
allowedHosts: [command.host],
|
|
3005
|
+
};
|
|
3006
|
+
const appJsonBinding = {
|
|
3007
|
+
id: command.id,
|
|
3008
|
+
label,
|
|
3009
|
+
name,
|
|
3010
|
+
source: bindingDirectory,
|
|
3011
|
+
interface: interfaceName,
|
|
3012
|
+
};
|
|
3013
|
+
const interfaceSource = `export interface ${interfaceName} {\n request(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;\n}\n`;
|
|
3014
|
+
const workerSource = `import { WorkerEntrypoint } from "cloudflare:workers";\n\nexport class ${interfaceName} extends WorkerEntrypoint {\n async request(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {\n const url = typeof input === "string" || input instanceof URL ? new URL(input) : new URL(input.url);\n if (url.hostname !== "${command.host}") {\n throw new Error("Host is not allowed for this binding.");\n }\n return fetch(url, init);\n }\n}\n`;
|
|
3015
|
+
return {
|
|
3016
|
+
ok: true,
|
|
3017
|
+
command: command.command,
|
|
3018
|
+
dryRun: command.dryRun,
|
|
3019
|
+
binding: {
|
|
3020
|
+
id: command.id,
|
|
3021
|
+
name,
|
|
3022
|
+
label,
|
|
3023
|
+
interface: interfaceName,
|
|
3024
|
+
allowedHosts: [command.host],
|
|
3025
|
+
permissions: ["outbound:http"],
|
|
3026
|
+
},
|
|
3027
|
+
appJsonBinding,
|
|
3028
|
+
files: [
|
|
3029
|
+
{
|
|
3030
|
+
path: `${bindingDirectory}/binding.json`,
|
|
3031
|
+
content: `${JSON.stringify(bindingJson, null, 2)}\n`,
|
|
3032
|
+
},
|
|
3033
|
+
{
|
|
3034
|
+
path: `${bindingDirectory}/interface.d.ts`,
|
|
3035
|
+
content: interfaceSource,
|
|
3036
|
+
},
|
|
3037
|
+
{
|
|
3038
|
+
path: `${bindingDirectory}/src/worker.ts`,
|
|
3039
|
+
content: workerSource,
|
|
3040
|
+
},
|
|
3041
|
+
],
|
|
3042
|
+
next: [
|
|
3043
|
+
"Add appJsonBinding to app.json bindings[].",
|
|
3044
|
+
`Call this binding from src/server.ts through env.${name}; do not fetch third-party URLs directly from src/server.ts.`,
|
|
3045
|
+
"Run tender app generate-env-types --dir . --json.",
|
|
3046
|
+
"Run tender app doctor --dir . --json.",
|
|
3047
|
+
],
|
|
3048
|
+
};
|
|
3049
|
+
}
|
|
3050
|
+
function runBindingsOutbound(command, io) {
|
|
3051
|
+
const payload = createOutboundBindingShape(command);
|
|
3052
|
+
if (command.json) {
|
|
3053
|
+
io.stdout(JSON.stringify(payload, null, 2));
|
|
3054
|
+
}
|
|
3055
|
+
else {
|
|
3056
|
+
io.stdout([
|
|
3057
|
+
`Outbound binding dry run: ${payload.binding.id}`,
|
|
3058
|
+
`app.json binding name: ${payload.binding.name}`,
|
|
3059
|
+
`allowed host: ${payload.binding.allowedHosts.join(", ")}`,
|
|
3060
|
+
"files:",
|
|
3061
|
+
...payload.files.map((file) => ` ${file.path}`),
|
|
3062
|
+
"next:",
|
|
3063
|
+
...payload.next.map((step) => ` ${step}`),
|
|
3064
|
+
].join("\n"));
|
|
3065
|
+
}
|
|
3066
|
+
return 0;
|
|
3067
|
+
}
|
|
2662
3068
|
async function parseJsonResponse(response) {
|
|
2663
3069
|
return (await response.json().catch(() => null));
|
|
2664
3070
|
}
|
|
@@ -4972,7 +5378,10 @@ export async function runTenderCli(args, io = {
|
|
|
4972
5378
|
try {
|
|
4973
5379
|
const command = parseTenderArgs(args);
|
|
4974
5380
|
if (command.command === "help") {
|
|
4975
|
-
if (command.topic === "
|
|
5381
|
+
if (command.topic === "capabilities") {
|
|
5382
|
+
io.stdout(capabilitiesHelp());
|
|
5383
|
+
}
|
|
5384
|
+
else if (command.topic === "auth login") {
|
|
4976
5385
|
io.stdout(authLoginHelp());
|
|
4977
5386
|
}
|
|
4978
5387
|
else if (command.topic === "auth status") {
|
|
@@ -5008,6 +5417,9 @@ export async function runTenderCli(args, io = {
|
|
|
5008
5417
|
else if (command.topic === "artifacts context refresh") {
|
|
5009
5418
|
io.stdout(artifactsContextRefreshHelp());
|
|
5010
5419
|
}
|
|
5420
|
+
else if (command.topic === "artifacts bindings outbound") {
|
|
5421
|
+
io.stdout(bindingsOutboundHelp());
|
|
5422
|
+
}
|
|
5011
5423
|
else if (command.topic === "artifacts git setup") {
|
|
5012
5424
|
io.stdout(gitSetupHelp());
|
|
5013
5425
|
}
|
|
@@ -5079,6 +5491,9 @@ export async function runTenderCli(args, io = {
|
|
|
5079
5491
|
}
|
|
5080
5492
|
return 0;
|
|
5081
5493
|
}
|
|
5494
|
+
if (command.command === "capabilities") {
|
|
5495
|
+
return runCapabilities(command, io);
|
|
5496
|
+
}
|
|
5082
5497
|
if (command.command === "auth login") {
|
|
5083
5498
|
return await runAuthLogin(command, io, runtime);
|
|
5084
5499
|
}
|
|
@@ -5109,6 +5524,9 @@ export async function runTenderCli(args, io = {
|
|
|
5109
5524
|
if (command.command === "artifacts context refresh") {
|
|
5110
5525
|
return await runContextFetch(command, io, runtime);
|
|
5111
5526
|
}
|
|
5527
|
+
if (command.command === "artifacts bindings outbound") {
|
|
5528
|
+
return runBindingsOutbound(command, io);
|
|
5529
|
+
}
|
|
5112
5530
|
if (command.command === "artifacts git setup") {
|
|
5113
5531
|
return await runGitSetup(command, io, runtime);
|
|
5114
5532
|
}
|
package/package.json
CHANGED