@tenderprompt/cli 0.1.12 → 0.1.14
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 +66 -0
- package/dist/index.js +804 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,6 +34,8 @@ session:
|
|
|
34
34
|
- build and preview a commit through the Tender API
|
|
35
35
|
- publish only after a dry run or explicit `--confirm publish`
|
|
36
36
|
- inspect artifact-scoped analytics without SQL or platform credentials
|
|
37
|
+
- inspect artifact-scoped app database tables, schema, and bounded read-only
|
|
38
|
+
query output
|
|
37
39
|
- run server-validated chart specs and export aggregate rows
|
|
38
40
|
- generate starter analytics suggestions
|
|
39
41
|
- create, list, update, and delete saved analytics dashboards and charts, with
|
|
@@ -87,6 +89,8 @@ TTL values are intentionally bounded:
|
|
|
87
89
|
|
|
88
90
|
Use `--profile publish` only when the token should be allowed to publish.
|
|
89
91
|
Publish still requires explicit confirmation with `--confirm publish`.
|
|
92
|
+
Use `--profile db-read --artifact <app-id>` for read-only app database
|
|
93
|
+
inspection.
|
|
90
94
|
|
|
91
95
|
Credential precedence is explicit-first:
|
|
92
96
|
|
|
@@ -128,6 +132,11 @@ tender app validate artifact_123 --json
|
|
|
128
132
|
tender app build artifact_123 --commit "$(git rev-parse HEAD)" --json
|
|
129
133
|
tender app preview artifact_123 --commit "$(git rev-parse HEAD)" --stream --json
|
|
130
134
|
tender app publish artifact_123 --dry-run --json
|
|
135
|
+
tender app db capabilities artifact_123 --json
|
|
136
|
+
tender app db tables artifact_123 --env published --json
|
|
137
|
+
tender app db schema artifact_123 --env published --table votes --json
|
|
138
|
+
tender app db query artifact_123 --env published --sql "select * from votes limit 20" --json
|
|
139
|
+
tender app db query artifact_123 --env published --sql "select * from votes order by created_at" --limit 50 --offset 50 --json
|
|
131
140
|
```
|
|
132
141
|
|
|
133
142
|
## Analytics
|
|
@@ -226,6 +235,61 @@ filter. `analytics query` is read-only, so use
|
|
|
226
235
|
query fails for a missing dashboard-query property, the CLI prints the exact
|
|
227
236
|
activation commands to run and does not perform that write automatically.
|
|
228
237
|
|
|
238
|
+
Event payload properties can be tracked by the app, observed in bounded catalog
|
|
239
|
+
samples, and still not be queryable in fast dashboard charts. Register intended
|
|
240
|
+
dashboard dimensions and metrics before meaningful traffic starts. Use
|
|
241
|
+
dimensions for grouping values such as variant, plan, step, source, or outcome,
|
|
242
|
+
and metrics for numeric values such as score, amount, duration, or quantity.
|
|
243
|
+
Making a property queryable does not require an app redeploy, but older
|
|
244
|
+
dashboard-query rows may not fully populate grouped charts until new traffic
|
|
245
|
+
arrives. Do not make every property queryable; avoid PII, secrets, free text,
|
|
246
|
+
ids, URLs, or high-cardinality values unless the chart has a clear purpose.
|
|
247
|
+
|
|
248
|
+
## App Database
|
|
249
|
+
|
|
250
|
+
App database commands are artifact-scoped and go through Tender's server-side
|
|
251
|
+
authorization and read-only query checks. The CLI only accepts an app id; it
|
|
252
|
+
does not accept tenant ids, runner names, provider account ids, or runtime
|
|
253
|
+
storage identifiers. Commands that read data require an explicit `--env preview`
|
|
254
|
+
or `--env published`; there is no default environment.
|
|
255
|
+
|
|
256
|
+
Start with capabilities:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
tender auth login --device --profile db-read --artifact artifact_123 --ttl 7d --save-profile db --json
|
|
260
|
+
|
|
261
|
+
tender app db capabilities artifact_123 --profile db --json
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Common read-only commands:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
tender app db tables artifact_123 --env published --profile db --json
|
|
268
|
+
|
|
269
|
+
tender app db schema artifact_123 --env published --table votes --profile db --json
|
|
270
|
+
|
|
271
|
+
tender app db query artifact_123 --env published --sql "select * from votes limit 20" --profile db --json
|
|
272
|
+
|
|
273
|
+
tender app db query artifact_123 \
|
|
274
|
+
--env published \
|
|
275
|
+
--sql "select * from votes where choice = ?" \
|
|
276
|
+
--param coffee \
|
|
277
|
+
--profile db \
|
|
278
|
+
--json
|
|
279
|
+
|
|
280
|
+
tender app db query artifact_123 --env published --file ./debug.sql --limit 50 --offset 100 --profile db --json
|
|
281
|
+
|
|
282
|
+
tender app db query artifact_123 --env published --file - --offset 100 --profile db --json < debug.sql
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
The query command sends one read-only statement with positional bindings. Query
|
|
286
|
+
responses include `requestedLimit`, `appliedLimit`, `maxInteractiveLimit`,
|
|
287
|
+
`offset`, and `nextOffset`; agents must request each page explicitly and should
|
|
288
|
+
not auto-drain large tables. The interactive row cap is 50 rows per request.
|
|
289
|
+
Client and server limits include a 100 KB statement cap and at most 100 bound
|
|
290
|
+
parameters. The server enforces the final policy, row limits, response limits,
|
|
291
|
+
artifact scope, and audit logging.
|
|
292
|
+
|
|
229
293
|
## Safety Model
|
|
230
294
|
|
|
231
295
|
- Local agents run in the user's environment, not in Tender.
|
|
@@ -242,6 +306,8 @@ activation commands to run and does not perform that write automatically.
|
|
|
242
306
|
tenant/artifact-scoped.
|
|
243
307
|
- Raw analytics event export is intentionally not available; exports are bounded
|
|
244
308
|
catalog metadata or validated aggregate query rows.
|
|
309
|
+
- App database commands require `db:read`, should use artifact-scoped tokens,
|
|
310
|
+
and never require provider account ids or runtime storage identifiers.
|
|
245
311
|
|
|
246
312
|
## Related Package
|
|
247
313
|
|
package/dist/index.js
CHANGED
|
@@ -41,16 +41,34 @@ class TenderCliUsageError extends Error {
|
|
|
41
41
|
}
|
|
42
42
|
class TenderCliAnalyticsApiError extends Error {
|
|
43
43
|
reasonCode;
|
|
44
|
-
|
|
44
|
+
scope;
|
|
45
|
+
example;
|
|
46
|
+
constructor(message, reasonCode, scope = null, example = null) {
|
|
45
47
|
super(message);
|
|
46
|
-
this.reasonCode = reasonCode;
|
|
47
48
|
this.name = "TenderCliAnalyticsApiError";
|
|
49
|
+
this.reasonCode = reasonCode;
|
|
50
|
+
this.scope = scope;
|
|
51
|
+
this.example = example;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
class TenderCliDbApiError extends Error {
|
|
55
|
+
reasonCode;
|
|
56
|
+
scope;
|
|
57
|
+
example;
|
|
58
|
+
constructor(message, reasonCode, scope, example) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.name = "TenderCliDbApiError";
|
|
61
|
+
this.reasonCode = reasonCode;
|
|
62
|
+
this.scope = scope;
|
|
63
|
+
this.example = example;
|
|
48
64
|
}
|
|
49
65
|
}
|
|
50
66
|
const DEFAULT_PROFILE_NAME = "default";
|
|
51
67
|
const DEFAULT_BASE_URL = "https://app.tenderprompt.com";
|
|
52
68
|
const ARTIFACT_SOURCE_EXPORT_MAX_FILES = 1000;
|
|
53
69
|
const ARTIFACT_SOURCE_EXPORT_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
70
|
+
const DB_SQL_STATEMENT_MAX_BYTES = 100 * 1024;
|
|
71
|
+
const DB_MAX_BOUND_PARAMETERS = 100;
|
|
54
72
|
function rootHelp() {
|
|
55
73
|
return `Usage: tender <command> [options]
|
|
56
74
|
|
|
@@ -89,6 +107,10 @@ Commands:
|
|
|
89
107
|
Run a server-validated chart spec
|
|
90
108
|
tender app analytics suggestions <app-id>
|
|
91
109
|
Generate useful starter chart suggestions
|
|
110
|
+
tender app db capabilities <app-id>
|
|
111
|
+
Describe app database query limits and policy
|
|
112
|
+
tender app db query <app-id> --env <preview|published> --sql <sql>
|
|
113
|
+
Run one read-only app database query
|
|
92
114
|
tender app context fetch <app-id>
|
|
93
115
|
Fetch managed agent context into a checkout
|
|
94
116
|
tender app context status <app-id>
|
|
@@ -120,6 +142,10 @@ Examples:
|
|
|
120
142
|
tender app analytics summary artifact_123 --range 30d --json
|
|
121
143
|
tender app analytics query artifact_123 --spec chart.json --json
|
|
122
144
|
tender app analytics suggestions artifact_123 --range 30d --json
|
|
145
|
+
tender app db capabilities artifact_123 --json
|
|
146
|
+
tender app db tables artifact_123 --env published --json
|
|
147
|
+
tender app db schema artifact_123 --env published --table votes --json
|
|
148
|
+
tender app db query artifact_123 --env published --sql "select * from votes limit 20" --json
|
|
123
149
|
tender app context fetch artifact_123 --dir ./widget --dry-run --json
|
|
124
150
|
tender app context status artifact_123 --dir ./widget --json
|
|
125
151
|
tender app context refresh artifact_123 --dir ./widget --dry-run --json
|
|
@@ -142,12 +168,12 @@ Examples:
|
|
|
142
168
|
tender auth status --json`;
|
|
143
169
|
}
|
|
144
170
|
function authLoginHelp() {
|
|
145
|
-
return `Usage: tender auth login --device [--base-url <url>] [--profile <edit-preview|publish|create>] [--artifact <artifact-id>] [--ttl <7d|14d|30d>] [--save-profile <name>] [--no-poll] [--json]
|
|
171
|
+
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]
|
|
146
172
|
|
|
147
173
|
Options:
|
|
148
174
|
--device Use the OAuth device-code flow.
|
|
149
175
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL or https://app.tenderprompt.com.
|
|
150
|
-
--profile <value> Token permission profile: edit-preview, publish, or
|
|
176
|
+
--profile <value> Token permission profile: edit-preview, publish, create, or db-read. Defaults to edit-preview.
|
|
151
177
|
--artifact <artifact-id> Scope the token to one artifact. Required for normal edit-preview agent work.
|
|
152
178
|
--ttl <value> Token lifetime: 7d, 14d, or 30d. Defaults to 7d.
|
|
153
179
|
--save-profile <name> Local config profile name. Defaults to default.
|
|
@@ -157,7 +183,8 @@ Options:
|
|
|
157
183
|
Examples:
|
|
158
184
|
tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d
|
|
159
185
|
tender auth login --device --profile publish --artifact artifact_123 --ttl 7d --save-profile publish --json
|
|
160
|
-
tender auth login --device --profile create --ttl 7d --save-profile account
|
|
186
|
+
tender auth login --device --profile create --ttl 7d --save-profile account
|
|
187
|
+
tender auth login --device --profile db-read --artifact artifact_123 --ttl 7d --save-profile db`;
|
|
161
188
|
}
|
|
162
189
|
function authStatusHelp() {
|
|
163
190
|
return `Usage: tender auth status [--profile <name>] [--json]
|
|
@@ -217,6 +244,14 @@ Commands:
|
|
|
217
244
|
List properties available to dashboard queries
|
|
218
245
|
tender app analytics properties activate <app-id>
|
|
219
246
|
Make a custom event property queryable
|
|
247
|
+
tender app db capabilities <app-id>
|
|
248
|
+
Describe app database query limits and policy
|
|
249
|
+
tender app db tables <app-id> --env <preview|published>
|
|
250
|
+
List app database tables and views
|
|
251
|
+
tender app db schema <app-id> --env <preview|published> --table <name>
|
|
252
|
+
Show table and index schema for one table
|
|
253
|
+
tender app db query <app-id> --env <preview|published> --sql <sql>
|
|
254
|
+
Run one read-only app database query
|
|
220
255
|
tender app context fetch <app-id>
|
|
221
256
|
Fetch AGENTS.md, skills, and Tender App scaffold files
|
|
222
257
|
tender app context status <app-id>
|
|
@@ -257,6 +292,10 @@ Examples:
|
|
|
257
292
|
tender app analytics charts delete artifact_123 --chart analytics_chart_123 --dry-run --json
|
|
258
293
|
tender app analytics properties list artifact_123 --json
|
|
259
294
|
tender app analytics properties activate artifact_123 --event signup_started --property plan --type dimension --dry-run --json
|
|
295
|
+
tender app db capabilities artifact_123 --json
|
|
296
|
+
tender app db tables artifact_123 --env published --json
|
|
297
|
+
tender app db schema artifact_123 --env published --table votes --json
|
|
298
|
+
tender app db query artifact_123 --env published --sql "select * from votes limit 20" --json
|
|
260
299
|
tender app context fetch artifact_123 --dir ./widget --dry-run --json
|
|
261
300
|
tender app context status artifact_123 --dir ./widget --json
|
|
262
301
|
tender app context refresh artifact_123 --dir ./widget --dry-run --json
|
|
@@ -435,6 +474,62 @@ Examples:
|
|
|
435
474
|
tender app analytics properties activate artifact_123 --event signup_started --property plan --type dimension --dry-run --json
|
|
436
475
|
tender app analytics properties activate artifact_123 --event score_saved --property score --type metric --json`;
|
|
437
476
|
}
|
|
477
|
+
function dbHelp() {
|
|
478
|
+
return `Usage: tender app db <command> <app-id> [options]
|
|
479
|
+
|
|
480
|
+
Commands:
|
|
481
|
+
tender app db capabilities <app-id> Describe app database policy and limits
|
|
482
|
+
tender app db tables <app-id> --env <preview|published>
|
|
483
|
+
List app database tables and views
|
|
484
|
+
tender app db schema <app-id> --env <preview|published>
|
|
485
|
+
Show schema for one table
|
|
486
|
+
tender app db query <app-id> --env <preview|published>
|
|
487
|
+
Run one read-only SQL query
|
|
488
|
+
|
|
489
|
+
Examples:
|
|
490
|
+
tender app db capabilities artifact_123 --json
|
|
491
|
+
tender app db tables artifact_123 --env published --json
|
|
492
|
+
tender app db schema artifact_123 --env published --table votes --json
|
|
493
|
+
tender app db query artifact_123 --env published --sql "select * from votes limit 20" --json
|
|
494
|
+
tender app db query artifact_123 --env published --file ./debug.sql --param active --limit 50 --json`;
|
|
495
|
+
}
|
|
496
|
+
function dbCapabilitiesHelp() {
|
|
497
|
+
return `Usage: tender app db capabilities <app-id> [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
498
|
+
|
|
499
|
+
Examples:
|
|
500
|
+
tender app db capabilities artifact_123 --json`;
|
|
501
|
+
}
|
|
502
|
+
function dbTablesHelp() {
|
|
503
|
+
return `Usage: tender app db tables <app-id> --env <preview|published> [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
504
|
+
|
|
505
|
+
Examples:
|
|
506
|
+
tender app db tables artifact_123 --env published --json
|
|
507
|
+
tender app db tables artifact_123 --env preview --json`;
|
|
508
|
+
}
|
|
509
|
+
function dbSchemaHelp() {
|
|
510
|
+
return `Usage: tender app db schema <app-id> --env <preview|published> --table <name> [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
511
|
+
|
|
512
|
+
Examples:
|
|
513
|
+
tender app db schema artifact_123 --env published --table votes --json
|
|
514
|
+
tender app db schema artifact_123 --table preference_votes --env preview --json`;
|
|
515
|
+
}
|
|
516
|
+
function dbQueryHelp() {
|
|
517
|
+
return `Usage: tender app db query <app-id> --env <preview|published> (--sql <sql> | --file <path|->) [--param <value>] [--limit <n>] [--offset <n>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
518
|
+
|
|
519
|
+
Options:
|
|
520
|
+
--env <value> Target app environment: preview or published. Required.
|
|
521
|
+
--sql <sql> SQL statement. Must be one read-only statement.
|
|
522
|
+
--file <path|-> Read SQL from a file or stdin with -.
|
|
523
|
+
--param <value> Bind one positional parameter. Repeat for multiple params. Values parse as numbers or null; other values are passed as strings.
|
|
524
|
+
--limit <n> Interactive row limit. Server caps the maximum at 50.
|
|
525
|
+
--offset <n> Skip rows before returning results. Server caps the maximum offset.
|
|
526
|
+
|
|
527
|
+
Examples:
|
|
528
|
+
tender app db query artifact_123 --env published --sql "select * from votes limit 20" --json
|
|
529
|
+
tender app db query artifact_123 --env published --sql "select * from votes where choice = ?" --param coffee --json
|
|
530
|
+
tender app db query artifact_123 --env published --file ./debug.sql --limit 50 --offset 100 --json
|
|
531
|
+
tender app db query artifact_123 --env published --file - --json < debug.sql`;
|
|
532
|
+
}
|
|
438
533
|
function artifactsListHelp() {
|
|
439
534
|
return `Usage: tender app list [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
440
535
|
|
|
@@ -767,6 +862,7 @@ function tenderCliCapabilities() {
|
|
|
767
862
|
"tender auth --help",
|
|
768
863
|
"tender app --help",
|
|
769
864
|
"tender app analytics --help",
|
|
865
|
+
"tender app db --help",
|
|
770
866
|
"tender app bindings outbound --help",
|
|
771
867
|
],
|
|
772
868
|
},
|
|
@@ -832,6 +928,23 @@ function tenderCliCapabilities() {
|
|
|
832
928
|
"Use list before writes so agents can avoid duplicates and explain the exact chart they are changing.",
|
|
833
929
|
],
|
|
834
930
|
},
|
|
931
|
+
{
|
|
932
|
+
name: "app_database_inspection",
|
|
933
|
+
when: "When inspecting app-local database tables from a scoped token.",
|
|
934
|
+
commands: [
|
|
935
|
+
"tender auth login --device --profile db-read --artifact <artifact-id> --ttl 7d --save-profile db --json",
|
|
936
|
+
"tender app db capabilities <artifact-id> --profile db --json",
|
|
937
|
+
"tender app db tables <artifact-id> --env published --profile db --json",
|
|
938
|
+
"tender app db schema <artifact-id> --env published --table <table-name> --profile db --json",
|
|
939
|
+
"tender app db query <artifact-id> --env published --sql \"select * from <table-name> limit 20\" --profile db --json",
|
|
940
|
+
"tender app db query <artifact-id> --env published --file ./debug.sql --profile db --json",
|
|
941
|
+
],
|
|
942
|
+
notes: [
|
|
943
|
+
"The CLI only accepts artifact ids. It never accepts tenant ids, runner names, provider account ids, or runtime storage identifiers.",
|
|
944
|
+
"Read-only policy is enforced by the server before the runtime shim executes SQL.",
|
|
945
|
+
"Use --file - to pipe SQL from stdin without prompts.",
|
|
946
|
+
],
|
|
947
|
+
},
|
|
835
948
|
{
|
|
836
949
|
name: "outbound_http_binding",
|
|
837
950
|
when: "When app code needs external APIs, OAuth/token exchange, webhooks, provider SDKs, or secrets.",
|
|
@@ -925,6 +1038,30 @@ function tenderCliCapabilities() {
|
|
|
925
1038
|
writes: true,
|
|
926
1039
|
dryRun: true,
|
|
927
1040
|
},
|
|
1041
|
+
{
|
|
1042
|
+
command: "tender app db capabilities <artifact-id> --json",
|
|
1043
|
+
purpose: "Inspect app database policy, limits, and supported capabilities for one artifact.",
|
|
1044
|
+
requiresAuth: true,
|
|
1045
|
+
writes: false,
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
command: "tender app db tables <artifact-id> --env published --json",
|
|
1049
|
+
purpose: "List app database tables and views for one artifact.",
|
|
1050
|
+
requiresAuth: true,
|
|
1051
|
+
writes: false,
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
command: "tender app db schema <artifact-id> --env published --table <table-name> --json",
|
|
1055
|
+
purpose: "Inspect table, index, and trigger schema for one app database table.",
|
|
1056
|
+
requiresAuth: true,
|
|
1057
|
+
writes: false,
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
command: "tender app db query <artifact-id> --env published --sql \"select * from <table-name> limit 20\" --json",
|
|
1061
|
+
purpose: "Run one bounded read-only app database query.",
|
|
1062
|
+
requiresAuth: true,
|
|
1063
|
+
writes: false,
|
|
1064
|
+
},
|
|
928
1065
|
],
|
|
929
1066
|
security: [
|
|
930
1067
|
"Prefer artifact-scoped device tokens.",
|
|
@@ -932,6 +1069,7 @@ function tenderCliCapabilities() {
|
|
|
932
1069
|
"Do not call Tender HTTP APIs directly from agent workflows.",
|
|
933
1070
|
"Do not put third-party fetch calls in src/server.ts; use outbound bindings.",
|
|
934
1071
|
"Do not request direct query credentials or raw SQL for analytics.",
|
|
1072
|
+
"Do not use tenant ids, runner names, provider account ids, or runtime storage identifiers for app database inspection.",
|
|
935
1073
|
],
|
|
936
1074
|
};
|
|
937
1075
|
}
|
|
@@ -2264,10 +2402,11 @@ function parseContextRefreshArgs(args) {
|
|
|
2264
2402
|
function parseTokenProfile(value) {
|
|
2265
2403
|
if (value === "edit-preview" ||
|
|
2266
2404
|
value === "publish" ||
|
|
2267
|
-
value === "create"
|
|
2405
|
+
value === "create" ||
|
|
2406
|
+
value === "db-read") {
|
|
2268
2407
|
return value;
|
|
2269
2408
|
}
|
|
2270
|
-
throw new TenderCliUsageError("--profile must be one of edit-preview, publish, or
|
|
2409
|
+
throw new TenderCliUsageError("--profile must be one of edit-preview, publish, create, or db-read.");
|
|
2271
2410
|
}
|
|
2272
2411
|
function parsePositiveIntegerFlag(value, flag) {
|
|
2273
2412
|
const parsed = Number(value);
|
|
@@ -2276,6 +2415,13 @@ function parsePositiveIntegerFlag(value, flag) {
|
|
|
2276
2415
|
}
|
|
2277
2416
|
return parsed;
|
|
2278
2417
|
}
|
|
2418
|
+
function parseNonNegativeIntegerFlag(value, flag) {
|
|
2419
|
+
const parsed = Number(value);
|
|
2420
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
2421
|
+
throw new TenderCliUsageError(`${flag} requires a non-negative integer.`);
|
|
2422
|
+
}
|
|
2423
|
+
return parsed;
|
|
2424
|
+
}
|
|
2279
2425
|
function parseAuthLoginArgs(args) {
|
|
2280
2426
|
if (args.includes("--help") || args.includes("-h")) {
|
|
2281
2427
|
return { command: "help", topic: "auth login" };
|
|
@@ -2341,6 +2487,9 @@ function parseAuthLoginArgs(args) {
|
|
|
2341
2487
|
if (tokenProfile === "create" && artifactId) {
|
|
2342
2488
|
throw new TenderCliUsageError("--profile create must be account-scoped; remove --artifact.");
|
|
2343
2489
|
}
|
|
2490
|
+
if (tokenProfile === "db-read" && !artifactId) {
|
|
2491
|
+
throw new TenderCliUsageError("--profile db-read requires --artifact <artifact-id>.");
|
|
2492
|
+
}
|
|
2344
2493
|
return {
|
|
2345
2494
|
command: "auth login",
|
|
2346
2495
|
baseUrl,
|
|
@@ -2941,10 +3090,12 @@ function analyticsChartTemplates() {
|
|
|
2941
3090
|
purpose: "Compare a queryable dimension property observed in events.",
|
|
2942
3091
|
usableAsIs: false,
|
|
2943
3092
|
placeholders: {
|
|
3093
|
+
eventName: "<event_name>",
|
|
2944
3094
|
property: "<queryable_dimension_property>",
|
|
2945
3095
|
},
|
|
2946
3096
|
adaptWith: [
|
|
2947
3097
|
"Use a queryable property key from capabilities chartSpec.groupBy, such as property:plan.",
|
|
3098
|
+
"Choose the event that owns the property and keep the event_name filter.",
|
|
2948
3099
|
"Choose stable categorical properties with enough observations to compare.",
|
|
2949
3100
|
],
|
|
2950
3101
|
spec: {
|
|
@@ -2953,7 +3104,7 @@ function analyticsChartTemplates() {
|
|
|
2953
3104
|
visualization: "bar",
|
|
2954
3105
|
metric: { name: "custom_event_count" },
|
|
2955
3106
|
groupBy: [{ field: "property:<queryable_dimension_property>" }],
|
|
2956
|
-
filters: [],
|
|
3107
|
+
filters: [{ field: "event_name", op: "=", value: "<event_name>" }],
|
|
2957
3108
|
range: { preset: "30d" },
|
|
2958
3109
|
sort: [{ field: "value", direction: "desc" }],
|
|
2959
3110
|
limit: 10,
|
|
@@ -2966,10 +3117,12 @@ function analyticsChartTemplates() {
|
|
|
2966
3117
|
purpose: "Track an observed numeric event property over time.",
|
|
2967
3118
|
usableAsIs: false,
|
|
2968
3119
|
placeholders: {
|
|
3120
|
+
eventName: "<event_name>",
|
|
2969
3121
|
metric: "avg:<queryable_metric_property>",
|
|
2970
3122
|
},
|
|
2971
3123
|
adaptWith: [
|
|
2972
3124
|
"Use a queryable numeric property key from capabilities chartSpec.metrics.",
|
|
3125
|
+
"Choose the event that owns the property and keep the event_name filter.",
|
|
2973
3126
|
"Prefer averages for scores, durations, quantities, or ordinal values.",
|
|
2974
3127
|
],
|
|
2975
3128
|
spec: {
|
|
@@ -2978,7 +3131,7 @@ function analyticsChartTemplates() {
|
|
|
2978
3131
|
visualization: "line",
|
|
2979
3132
|
metric: { name: "avg:<queryable_metric_property>" },
|
|
2980
3133
|
groupBy: [{ field: "day" }],
|
|
2981
|
-
filters: [],
|
|
3134
|
+
filters: [{ field: "event_name", op: "=", value: "<event_name>" }],
|
|
2982
3135
|
range: { preset: "30d" },
|
|
2983
3136
|
sort: [{ field: "label", direction: "asc" }],
|
|
2984
3137
|
limit: 30,
|
|
@@ -3292,6 +3445,303 @@ function parseAnalyticsPropertiesActivateArgs(args) {
|
|
|
3292
3445
|
...parsed,
|
|
3293
3446
|
};
|
|
3294
3447
|
}
|
|
3448
|
+
function parseDbCommonArgs(args, startIndex) {
|
|
3449
|
+
let baseUrl = null;
|
|
3450
|
+
let flagToken = null;
|
|
3451
|
+
let profileName = null;
|
|
3452
|
+
let json = false;
|
|
3453
|
+
const remaining = [];
|
|
3454
|
+
for (let index = startIndex; index < args.length; index += 1) {
|
|
3455
|
+
const arg = args[index];
|
|
3456
|
+
if (arg === "--json") {
|
|
3457
|
+
json = true;
|
|
3458
|
+
continue;
|
|
3459
|
+
}
|
|
3460
|
+
if (arg === "--base-url") {
|
|
3461
|
+
baseUrl = parseBaseUrlFlag(args, index);
|
|
3462
|
+
index += 1;
|
|
3463
|
+
continue;
|
|
3464
|
+
}
|
|
3465
|
+
if (arg === "--token") {
|
|
3466
|
+
flagToken = parseTokenFlag(args, index);
|
|
3467
|
+
index += 1;
|
|
3468
|
+
continue;
|
|
3469
|
+
}
|
|
3470
|
+
if (arg === "--profile") {
|
|
3471
|
+
profileName = parseProfileName(args[index + 1], "--profile");
|
|
3472
|
+
index += 1;
|
|
3473
|
+
continue;
|
|
3474
|
+
}
|
|
3475
|
+
remaining.push(arg);
|
|
3476
|
+
}
|
|
3477
|
+
return { baseUrl, flagToken, profileName, json, remaining };
|
|
3478
|
+
}
|
|
3479
|
+
function parseDbEnvironment(value) {
|
|
3480
|
+
if (!value || value.startsWith("--")) {
|
|
3481
|
+
throw new TenderCliUsageError("--env requires a value. Use preview or published.");
|
|
3482
|
+
}
|
|
3483
|
+
if (value === "preview") {
|
|
3484
|
+
return "preview";
|
|
3485
|
+
}
|
|
3486
|
+
if (value === "published" ||
|
|
3487
|
+
value === "live" ||
|
|
3488
|
+
value === "production" ||
|
|
3489
|
+
value === "prod") {
|
|
3490
|
+
return "published";
|
|
3491
|
+
}
|
|
3492
|
+
throw new TenderCliUsageError("--env must be preview or published.");
|
|
3493
|
+
}
|
|
3494
|
+
function parseFlagAssignment(arg, flag) {
|
|
3495
|
+
const prefix = `${flag}=`;
|
|
3496
|
+
return arg.startsWith(prefix) ? arg.slice(prefix.length) : null;
|
|
3497
|
+
}
|
|
3498
|
+
function parseDbFlagValue(value, flag) {
|
|
3499
|
+
if (!value || value.startsWith("--")) {
|
|
3500
|
+
throw new TenderCliUsageError(`${flag} requires a value.`);
|
|
3501
|
+
}
|
|
3502
|
+
return value;
|
|
3503
|
+
}
|
|
3504
|
+
function parseDbSqlFlagValue(value) {
|
|
3505
|
+
if (value === undefined || value.length === 0) {
|
|
3506
|
+
throw new TenderCliUsageError("--sql requires a value.");
|
|
3507
|
+
}
|
|
3508
|
+
return value;
|
|
3509
|
+
}
|
|
3510
|
+
function parseDbFilePath(value) {
|
|
3511
|
+
if (!value || (value !== "-" && value.startsWith("-"))) {
|
|
3512
|
+
throw new TenderCliUsageError("--file requires a SQL file path or - for stdin.");
|
|
3513
|
+
}
|
|
3514
|
+
return value;
|
|
3515
|
+
}
|
|
3516
|
+
function parseDbParameter(value) {
|
|
3517
|
+
const raw = parseDbFlagValue(value, "--param");
|
|
3518
|
+
try {
|
|
3519
|
+
const parsed = JSON.parse(raw);
|
|
3520
|
+
if (parsed === null ||
|
|
3521
|
+
typeof parsed === "string" ||
|
|
3522
|
+
typeof parsed === "number") {
|
|
3523
|
+
return parsed;
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
catch {
|
|
3527
|
+
// Fall back to a string binding.
|
|
3528
|
+
}
|
|
3529
|
+
return raw;
|
|
3530
|
+
}
|
|
3531
|
+
function parseDbCapabilitiesArgs(args) {
|
|
3532
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3533
|
+
return { command: "help", topic: "app db capabilities" };
|
|
3534
|
+
}
|
|
3535
|
+
const artifactId = args[0];
|
|
3536
|
+
if (!artifactId || artifactId.startsWith("-")) {
|
|
3537
|
+
throw new TenderCliUsageError("app id is required. Example: tender app db capabilities <app-id> --json");
|
|
3538
|
+
}
|
|
3539
|
+
const parsed = parseDbCommonArgs(args, 1);
|
|
3540
|
+
if (parsed.remaining.length > 0) {
|
|
3541
|
+
throw new TenderCliUsageError(`Unknown app db capabilities option: ${parsed.remaining[0]}`);
|
|
3542
|
+
}
|
|
3543
|
+
return { command: "app db capabilities", artifactId, ...parsed };
|
|
3544
|
+
}
|
|
3545
|
+
function parseDbTablesArgs(args) {
|
|
3546
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3547
|
+
return { command: "help", topic: "app db tables" };
|
|
3548
|
+
}
|
|
3549
|
+
const artifactId = args[0];
|
|
3550
|
+
if (!artifactId || artifactId.startsWith("-")) {
|
|
3551
|
+
throw new TenderCliUsageError("app id is required. Example: tender app db tables <app-id> --env published --json");
|
|
3552
|
+
}
|
|
3553
|
+
const parsed = parseDbCommonArgs(args, 1);
|
|
3554
|
+
let environment = null;
|
|
3555
|
+
for (let index = 0; index < parsed.remaining.length; index += 1) {
|
|
3556
|
+
const arg = parsed.remaining[index];
|
|
3557
|
+
const envAssignment = parseFlagAssignment(arg, "--env");
|
|
3558
|
+
if (envAssignment !== null) {
|
|
3559
|
+
environment = parseDbEnvironment(envAssignment);
|
|
3560
|
+
continue;
|
|
3561
|
+
}
|
|
3562
|
+
if (arg === "--env") {
|
|
3563
|
+
environment = parseDbEnvironment(parsed.remaining[index + 1]);
|
|
3564
|
+
index += 1;
|
|
3565
|
+
continue;
|
|
3566
|
+
}
|
|
3567
|
+
throw new TenderCliUsageError(`Unknown app db tables option: ${arg}`);
|
|
3568
|
+
}
|
|
3569
|
+
if (!environment) {
|
|
3570
|
+
throw new TenderCliUsageError("--env is required. Example: tender app db tables artifact_123 --env published --json");
|
|
3571
|
+
}
|
|
3572
|
+
return { command: "app db tables", artifactId, environment, ...parsed };
|
|
3573
|
+
}
|
|
3574
|
+
function parseDbSchemaArgs(args) {
|
|
3575
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3576
|
+
return { command: "help", topic: "app db schema" };
|
|
3577
|
+
}
|
|
3578
|
+
const artifactId = args[0];
|
|
3579
|
+
if (!artifactId || artifactId.startsWith("-")) {
|
|
3580
|
+
throw new TenderCliUsageError("app id is required. Example: tender app db schema <app-id> --env published --table votes --json");
|
|
3581
|
+
}
|
|
3582
|
+
const parsed = parseDbCommonArgs(args, 1);
|
|
3583
|
+
let environment = null;
|
|
3584
|
+
let tableName = null;
|
|
3585
|
+
for (let index = 0; index < parsed.remaining.length; index += 1) {
|
|
3586
|
+
const arg = parsed.remaining[index];
|
|
3587
|
+
const envAssignment = parseFlagAssignment(arg, "--env");
|
|
3588
|
+
if (envAssignment !== null) {
|
|
3589
|
+
environment = parseDbEnvironment(envAssignment);
|
|
3590
|
+
continue;
|
|
3591
|
+
}
|
|
3592
|
+
const tableAssignment = parseFlagAssignment(arg, "--table");
|
|
3593
|
+
if (tableAssignment !== null) {
|
|
3594
|
+
tableName = parseRequiredFlagValue(tableAssignment, "--table");
|
|
3595
|
+
continue;
|
|
3596
|
+
}
|
|
3597
|
+
if (arg === "--env") {
|
|
3598
|
+
environment = parseDbEnvironment(parsed.remaining[index + 1]);
|
|
3599
|
+
index += 1;
|
|
3600
|
+
continue;
|
|
3601
|
+
}
|
|
3602
|
+
if (arg === "--table") {
|
|
3603
|
+
tableName = parseRequiredFlagValue(parsed.remaining[index + 1], "--table");
|
|
3604
|
+
index += 1;
|
|
3605
|
+
continue;
|
|
3606
|
+
}
|
|
3607
|
+
throw new TenderCliUsageError(`Unknown app db schema option: ${arg}`);
|
|
3608
|
+
}
|
|
3609
|
+
if (!environment) {
|
|
3610
|
+
throw new TenderCliUsageError("--env is required. Example: tender app db schema artifact_123 --env published --table votes --json");
|
|
3611
|
+
}
|
|
3612
|
+
if (!tableName) {
|
|
3613
|
+
throw new TenderCliUsageError("--table is required. Example: tender app db schema artifact_123 --env published --table votes --json");
|
|
3614
|
+
}
|
|
3615
|
+
return {
|
|
3616
|
+
command: "app db schema",
|
|
3617
|
+
artifactId,
|
|
3618
|
+
environment,
|
|
3619
|
+
tableName,
|
|
3620
|
+
...parsed,
|
|
3621
|
+
};
|
|
3622
|
+
}
|
|
3623
|
+
function parseDbQueryArgs(args) {
|
|
3624
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3625
|
+
return { command: "help", topic: "app db query" };
|
|
3626
|
+
}
|
|
3627
|
+
const artifactId = args[0];
|
|
3628
|
+
if (!artifactId || artifactId.startsWith("-")) {
|
|
3629
|
+
throw new TenderCliUsageError("app id is required. Example: tender app db query <app-id> --env published --sql \"select * from votes limit 20\" --json");
|
|
3630
|
+
}
|
|
3631
|
+
const parsed = parseDbCommonArgs(args, 1);
|
|
3632
|
+
let environment = null;
|
|
3633
|
+
let sqlSource = null;
|
|
3634
|
+
const params = [];
|
|
3635
|
+
let limit = null;
|
|
3636
|
+
let offset = 0;
|
|
3637
|
+
for (let index = 0; index < parsed.remaining.length; index += 1) {
|
|
3638
|
+
const arg = parsed.remaining[index];
|
|
3639
|
+
const envAssignment = parseFlagAssignment(arg, "--env");
|
|
3640
|
+
if (envAssignment !== null) {
|
|
3641
|
+
environment = parseDbEnvironment(envAssignment);
|
|
3642
|
+
continue;
|
|
3643
|
+
}
|
|
3644
|
+
const sqlAssignment = parseFlagAssignment(arg, "--sql");
|
|
3645
|
+
if (sqlAssignment !== null) {
|
|
3646
|
+
if (sqlSource) {
|
|
3647
|
+
throw new TenderCliUsageError("--sql and --file cannot be repeated or combined.");
|
|
3648
|
+
}
|
|
3649
|
+
sqlSource = {
|
|
3650
|
+
kind: "inline",
|
|
3651
|
+
value: parseDbSqlFlagValue(sqlAssignment),
|
|
3652
|
+
};
|
|
3653
|
+
continue;
|
|
3654
|
+
}
|
|
3655
|
+
const fileAssignment = parseFlagAssignment(arg, "--file");
|
|
3656
|
+
if (fileAssignment !== null) {
|
|
3657
|
+
if (sqlSource) {
|
|
3658
|
+
throw new TenderCliUsageError("--sql and --file cannot be repeated or combined.");
|
|
3659
|
+
}
|
|
3660
|
+
sqlSource = {
|
|
3661
|
+
kind: "file",
|
|
3662
|
+
value: parseDbFilePath(fileAssignment),
|
|
3663
|
+
};
|
|
3664
|
+
continue;
|
|
3665
|
+
}
|
|
3666
|
+
const paramAssignment = parseFlagAssignment(arg, "--param");
|
|
3667
|
+
if (paramAssignment !== null) {
|
|
3668
|
+
params.push(parseDbParameter(paramAssignment));
|
|
3669
|
+
continue;
|
|
3670
|
+
}
|
|
3671
|
+
const limitAssignment = parseFlagAssignment(arg, "--limit");
|
|
3672
|
+
if (limitAssignment !== null) {
|
|
3673
|
+
limit = parsePositiveIntegerFlag(limitAssignment, "--limit");
|
|
3674
|
+
continue;
|
|
3675
|
+
}
|
|
3676
|
+
const offsetAssignment = parseFlagAssignment(arg, "--offset");
|
|
3677
|
+
if (offsetAssignment !== null) {
|
|
3678
|
+
offset = parseNonNegativeIntegerFlag(offsetAssignment, "--offset");
|
|
3679
|
+
continue;
|
|
3680
|
+
}
|
|
3681
|
+
if (arg === "--env") {
|
|
3682
|
+
environment = parseDbEnvironment(parsed.remaining[index + 1]);
|
|
3683
|
+
index += 1;
|
|
3684
|
+
continue;
|
|
3685
|
+
}
|
|
3686
|
+
if (arg === "--sql") {
|
|
3687
|
+
if (sqlSource) {
|
|
3688
|
+
throw new TenderCliUsageError("--sql and --file cannot be repeated or combined.");
|
|
3689
|
+
}
|
|
3690
|
+
sqlSource = {
|
|
3691
|
+
kind: "inline",
|
|
3692
|
+
value: parseDbSqlFlagValue(parsed.remaining[index + 1]),
|
|
3693
|
+
};
|
|
3694
|
+
index += 1;
|
|
3695
|
+
continue;
|
|
3696
|
+
}
|
|
3697
|
+
if (arg === "--file") {
|
|
3698
|
+
if (sqlSource) {
|
|
3699
|
+
throw new TenderCliUsageError("--sql and --file cannot be repeated or combined.");
|
|
3700
|
+
}
|
|
3701
|
+
sqlSource = {
|
|
3702
|
+
kind: "file",
|
|
3703
|
+
value: parseDbFilePath(parsed.remaining[index + 1]),
|
|
3704
|
+
};
|
|
3705
|
+
index += 1;
|
|
3706
|
+
continue;
|
|
3707
|
+
}
|
|
3708
|
+
if (arg === "--param") {
|
|
3709
|
+
params.push(parseDbParameter(parsed.remaining[index + 1]));
|
|
3710
|
+
index += 1;
|
|
3711
|
+
continue;
|
|
3712
|
+
}
|
|
3713
|
+
if (arg === "--limit") {
|
|
3714
|
+
limit = parsePositiveIntegerFlag(parsed.remaining[index + 1], "--limit");
|
|
3715
|
+
index += 1;
|
|
3716
|
+
continue;
|
|
3717
|
+
}
|
|
3718
|
+
if (arg === "--offset") {
|
|
3719
|
+
offset = parseNonNegativeIntegerFlag(parsed.remaining[index + 1], "--offset");
|
|
3720
|
+
index += 1;
|
|
3721
|
+
continue;
|
|
3722
|
+
}
|
|
3723
|
+
throw new TenderCliUsageError(`Unknown app db query option: ${arg}`);
|
|
3724
|
+
}
|
|
3725
|
+
if (!environment) {
|
|
3726
|
+
throw new TenderCliUsageError("--env is required. Example: tender app db query artifact_123 --env published --sql \"select * from votes limit 20\" --json");
|
|
3727
|
+
}
|
|
3728
|
+
if (!sqlSource) {
|
|
3729
|
+
throw new TenderCliUsageError("--sql or --file is required. Example: tender app db query artifact_123 --env published --sql \"select * from votes limit 20\" --json");
|
|
3730
|
+
}
|
|
3731
|
+
if (params.length > DB_MAX_BOUND_PARAMETERS) {
|
|
3732
|
+
throw new TenderCliUsageError(`App database queries are limited to ${DB_MAX_BOUND_PARAMETERS} bound parameters.`);
|
|
3733
|
+
}
|
|
3734
|
+
return {
|
|
3735
|
+
command: "app db query",
|
|
3736
|
+
artifactId,
|
|
3737
|
+
environment,
|
|
3738
|
+
sqlSource,
|
|
3739
|
+
params,
|
|
3740
|
+
limit,
|
|
3741
|
+
offset,
|
|
3742
|
+
...parsed,
|
|
3743
|
+
};
|
|
3744
|
+
}
|
|
3295
3745
|
function parseTenderArgs(args) {
|
|
3296
3746
|
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
3297
3747
|
return { command: "help", topic: "root" };
|
|
@@ -3441,6 +3891,27 @@ function parseTenderArgs(args) {
|
|
|
3441
3891
|
resourceArgs[3] === "delete") {
|
|
3442
3892
|
return parseAnalyticsChartsDeleteArgs(resourceArgs.slice(4));
|
|
3443
3893
|
}
|
|
3894
|
+
if (resourceArgs[1] === "db" &&
|
|
3895
|
+
(resourceArgs[2] === undefined ||
|
|
3896
|
+
resourceArgs[2] === "--help" ||
|
|
3897
|
+
resourceArgs[2] === "-h")) {
|
|
3898
|
+
return { command: "help", topic: "app db" };
|
|
3899
|
+
}
|
|
3900
|
+
if (resourceArgs[1] === "db" && resourceArgs[2] === "capabilities") {
|
|
3901
|
+
return parseDbCapabilitiesArgs(resourceArgs.slice(3));
|
|
3902
|
+
}
|
|
3903
|
+
if (resourceArgs[1] === "db" && resourceArgs[2] === "tables") {
|
|
3904
|
+
return parseDbTablesArgs(resourceArgs.slice(3));
|
|
3905
|
+
}
|
|
3906
|
+
if (resourceArgs[1] === "db" && resourceArgs[2] === "schema") {
|
|
3907
|
+
return parseDbSchemaArgs(resourceArgs.slice(3));
|
|
3908
|
+
}
|
|
3909
|
+
if (resourceArgs[1] === "db" && resourceArgs[2] === "query") {
|
|
3910
|
+
return parseDbQueryArgs(resourceArgs.slice(3));
|
|
3911
|
+
}
|
|
3912
|
+
if (resourceArgs[1] === "db") {
|
|
3913
|
+
throw new TenderCliUsageError(`Unknown app db command: ${resourceArgs[2]}`);
|
|
3914
|
+
}
|
|
3444
3915
|
if (resourceArgs[1] === "context" &&
|
|
3445
3916
|
(resourceArgs[2] === undefined ||
|
|
3446
3917
|
resourceArgs[2] === "--help" ||
|
|
@@ -3833,15 +4304,72 @@ async function requestAnalyticsApi(input) {
|
|
|
3833
4304
|
const message = (typeof payload?.message === "string" && payload.message) ||
|
|
3834
4305
|
(typeof payload?.reasonCode === "string" && payload.reasonCode) ||
|
|
3835
4306
|
`Analytics request failed with HTTP ${response.status}.`;
|
|
4307
|
+
const example = analyticsCorrectedExample(input.artifactId, input.analyticsPath);
|
|
3836
4308
|
throw new TenderCliAnalyticsApiError([
|
|
3837
4309
|
message,
|
|
3838
4310
|
`Scope: artifact ${input.artifactId}, analytics/${input.analyticsPath || "summary"}.`,
|
|
3839
4311
|
`Reason: ${reasonCode}.`,
|
|
3840
|
-
`Example: ${
|
|
3841
|
-
].join("\n"), reasonCode
|
|
4312
|
+
`Example: ${example}.`,
|
|
4313
|
+
].join("\n"), reasonCode, {
|
|
4314
|
+
artifactId: input.artifactId,
|
|
4315
|
+
surface: `analytics/${input.analyticsPath || "summary"}`,
|
|
4316
|
+
}, example);
|
|
3842
4317
|
}
|
|
3843
4318
|
return payload;
|
|
3844
4319
|
}
|
|
4320
|
+
function appendDbEnvironment(params, environment) {
|
|
4321
|
+
params.set("env", environment);
|
|
4322
|
+
}
|
|
4323
|
+
async function requestDbApi(input) {
|
|
4324
|
+
const params = new URLSearchParams();
|
|
4325
|
+
if (input.environment) {
|
|
4326
|
+
appendDbEnvironment(params, input.environment);
|
|
4327
|
+
}
|
|
4328
|
+
if (input.tableName) {
|
|
4329
|
+
params.set("table", input.tableName);
|
|
4330
|
+
}
|
|
4331
|
+
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
4332
|
+
const response = await input.fetcher(`${input.baseUrl}/api/v1/artifacts/${encodeURIComponent(input.artifactId)}/db/${input.dbPath}${query}`, {
|
|
4333
|
+
method: input.method,
|
|
4334
|
+
headers: {
|
|
4335
|
+
authorization: `Bearer ${input.token}`,
|
|
4336
|
+
accept: "application/json",
|
|
4337
|
+
...(input.body === undefined ? {} : { "content-type": "application/json" }),
|
|
4338
|
+
},
|
|
4339
|
+
body: input.body === undefined ? undefined : JSON.stringify(input.body),
|
|
4340
|
+
});
|
|
4341
|
+
const payload = await parseJsonResponse(response);
|
|
4342
|
+
if (!response.ok || !payload?.ok) {
|
|
4343
|
+
const reasonCode = (typeof payload?.reasonCode === "string" && payload.reasonCode) ||
|
|
4344
|
+
`http_${response.status}`;
|
|
4345
|
+
const message = (typeof payload?.message === "string" && payload.message) ||
|
|
4346
|
+
(typeof payload?.reasonCode === "string" && payload.reasonCode) ||
|
|
4347
|
+
`App database request failed with HTTP ${response.status}.`;
|
|
4348
|
+
const example = dbCorrectedExample(input.artifactId, input.dbPath);
|
|
4349
|
+
throw new TenderCliDbApiError([
|
|
4350
|
+
message,
|
|
4351
|
+
`Scope: artifact ${input.artifactId}, db/${input.dbPath}.`,
|
|
4352
|
+
`Reason: ${reasonCode}.`,
|
|
4353
|
+
`Example: ${example}.`,
|
|
4354
|
+
].join("\n"), reasonCode, {
|
|
4355
|
+
artifactId: input.artifactId,
|
|
4356
|
+
surface: `db/${input.dbPath}`,
|
|
4357
|
+
}, example);
|
|
4358
|
+
}
|
|
4359
|
+
return payload;
|
|
4360
|
+
}
|
|
4361
|
+
function dbCorrectedExample(artifactId, dbPath) {
|
|
4362
|
+
if (dbPath === "capabilities") {
|
|
4363
|
+
return `tender app db capabilities ${artifactId} --json`;
|
|
4364
|
+
}
|
|
4365
|
+
if (dbPath === "tables") {
|
|
4366
|
+
return `tender app db tables ${artifactId} --env published --json`;
|
|
4367
|
+
}
|
|
4368
|
+
if (dbPath === "schema") {
|
|
4369
|
+
return `tender app db schema ${artifactId} --env published --table <table-name> --json`;
|
|
4370
|
+
}
|
|
4371
|
+
return `tender app db query ${artifactId} --env published --sql "select * from <table-name> limit 20" --json`;
|
|
4372
|
+
}
|
|
3845
4373
|
function analyticsCorrectedExample(artifactId, analyticsPath) {
|
|
3846
4374
|
if (analyticsPath === "capabilities") {
|
|
3847
4375
|
return `tender app analytics capabilities ${artifactId} --include-catalog --range 30d --json`;
|
|
@@ -4089,7 +4617,7 @@ async function requestAnalyticsQueryWithPropertyGuidance(input) {
|
|
|
4089
4617
|
spec: input.spec,
|
|
4090
4618
|
});
|
|
4091
4619
|
if (guidance) {
|
|
4092
|
-
throw new TenderCliAnalyticsApiError(`${error.message}\n\n${guidance}`, error.reasonCode);
|
|
4620
|
+
throw new TenderCliAnalyticsApiError(`${error.message}\n\n${guidance}`, error.reasonCode, error.scope, error.example);
|
|
4093
4621
|
}
|
|
4094
4622
|
}
|
|
4095
4623
|
throw error;
|
|
@@ -6127,6 +6655,137 @@ async function resolveAnalyticsCredentials(command, runtime) {
|
|
|
6127
6655
|
env: runtime.env ?? process.env,
|
|
6128
6656
|
});
|
|
6129
6657
|
}
|
|
6658
|
+
async function resolveDbCredentials(command, runtime) {
|
|
6659
|
+
return await resolveApiCredentials({
|
|
6660
|
+
baseUrl: command.baseUrl,
|
|
6661
|
+
flagToken: command.flagToken,
|
|
6662
|
+
profileName: command.profileName,
|
|
6663
|
+
env: runtime.env ?? process.env,
|
|
6664
|
+
});
|
|
6665
|
+
}
|
|
6666
|
+
function validateDbSql(sql) {
|
|
6667
|
+
if (!sql.trim()) {
|
|
6668
|
+
throw new TenderCliUsageError("SQL statement is required.");
|
|
6669
|
+
}
|
|
6670
|
+
const byteLength = new TextEncoder().encode(sql).byteLength;
|
|
6671
|
+
if (byteLength > DB_SQL_STATEMENT_MAX_BYTES) {
|
|
6672
|
+
throw new TenderCliUsageError(`SQL statement is limited to ${DB_SQL_STATEMENT_MAX_BYTES} bytes.`);
|
|
6673
|
+
}
|
|
6674
|
+
}
|
|
6675
|
+
async function readDbSql(command, runtime) {
|
|
6676
|
+
const sql = command.sqlSource.kind === "inline"
|
|
6677
|
+
? command.sqlSource.value
|
|
6678
|
+
: command.sqlSource.value === "-"
|
|
6679
|
+
? runtime.stdin
|
|
6680
|
+
: await readFile(path.resolve(command.sqlSource.value), "utf8");
|
|
6681
|
+
if (!sql) {
|
|
6682
|
+
throw new TenderCliUsageError("--file - requires SQL on stdin.");
|
|
6683
|
+
}
|
|
6684
|
+
validateDbSql(sql);
|
|
6685
|
+
return sql;
|
|
6686
|
+
}
|
|
6687
|
+
function printDbPayload(command, io, payload, fallbackLines) {
|
|
6688
|
+
if (command.json) {
|
|
6689
|
+
io.stdout(JSON.stringify(payload, null, 2));
|
|
6690
|
+
}
|
|
6691
|
+
else {
|
|
6692
|
+
io.stdout(fallbackLines.join("\n"));
|
|
6693
|
+
}
|
|
6694
|
+
}
|
|
6695
|
+
async function runDbCapabilities(command, io, runtime) {
|
|
6696
|
+
const credentials = await resolveDbCredentials(command, runtime);
|
|
6697
|
+
const payload = await requestDbApi({
|
|
6698
|
+
artifactId: command.artifactId,
|
|
6699
|
+
dbPath: "capabilities",
|
|
6700
|
+
method: "GET",
|
|
6701
|
+
baseUrl: credentials.baseUrl,
|
|
6702
|
+
token: credentials.token,
|
|
6703
|
+
fetcher: runtime.fetcher ?? fetch,
|
|
6704
|
+
});
|
|
6705
|
+
const limits = isCliJsonRecord(payload.limits) ? payload.limits : {};
|
|
6706
|
+
const runtimeInfo = isCliJsonRecord(payload.runtime) ? payload.runtime : {};
|
|
6707
|
+
printDbPayload(command, io, payload, [
|
|
6708
|
+
`app_id: ${command.artifactId}`,
|
|
6709
|
+
`backend: ${String(runtimeInfo.backend ?? "app-database")}`,
|
|
6710
|
+
`default_limit: ${String(limits.defaultLimit ?? "")}`,
|
|
6711
|
+
`max_limit: ${String(limits.maxInteractiveLimit ?? "")}`,
|
|
6712
|
+
]);
|
|
6713
|
+
return 0;
|
|
6714
|
+
}
|
|
6715
|
+
async function runDbTables(command, io, runtime) {
|
|
6716
|
+
const credentials = await resolveDbCredentials(command, runtime);
|
|
6717
|
+
const payload = await requestDbApi({
|
|
6718
|
+
artifactId: command.artifactId,
|
|
6719
|
+
dbPath: "tables",
|
|
6720
|
+
method: "GET",
|
|
6721
|
+
baseUrl: credentials.baseUrl,
|
|
6722
|
+
token: credentials.token,
|
|
6723
|
+
fetcher: runtime.fetcher ?? fetch,
|
|
6724
|
+
environment: command.environment,
|
|
6725
|
+
});
|
|
6726
|
+
const rows = Array.isArray(payload.rows) ? payload.rows : [];
|
|
6727
|
+
printDbPayload(command, io, payload, [
|
|
6728
|
+
`app_id: ${command.artifactId}`,
|
|
6729
|
+
`environment: ${command.environment}`,
|
|
6730
|
+
`tables: ${rows.length}`,
|
|
6731
|
+
]);
|
|
6732
|
+
return 0;
|
|
6733
|
+
}
|
|
6734
|
+
async function runDbSchema(command, io, runtime) {
|
|
6735
|
+
const credentials = await resolveDbCredentials(command, runtime);
|
|
6736
|
+
const payload = await requestDbApi({
|
|
6737
|
+
artifactId: command.artifactId,
|
|
6738
|
+
dbPath: "schema",
|
|
6739
|
+
method: "GET",
|
|
6740
|
+
baseUrl: credentials.baseUrl,
|
|
6741
|
+
token: credentials.token,
|
|
6742
|
+
fetcher: runtime.fetcher ?? fetch,
|
|
6743
|
+
environment: command.environment,
|
|
6744
|
+
tableName: command.tableName,
|
|
6745
|
+
});
|
|
6746
|
+
const rows = Array.isArray(payload.rows) ? payload.rows : [];
|
|
6747
|
+
printDbPayload(command, io, payload, [
|
|
6748
|
+
`app_id: ${command.artifactId}`,
|
|
6749
|
+
`environment: ${command.environment}`,
|
|
6750
|
+
`table: ${command.tableName}`,
|
|
6751
|
+
`schema_rows: ${rows.length}`,
|
|
6752
|
+
]);
|
|
6753
|
+
return 0;
|
|
6754
|
+
}
|
|
6755
|
+
async function runDbQuery(command, io, runtime) {
|
|
6756
|
+
const credentials = await resolveDbCredentials(command, runtime);
|
|
6757
|
+
const sql = await readDbSql(command, runtime);
|
|
6758
|
+
const body = {
|
|
6759
|
+
environment: command.environment,
|
|
6760
|
+
mode: "read",
|
|
6761
|
+
sql,
|
|
6762
|
+
params: command.params,
|
|
6763
|
+
...(command.limit === null ? {} : { limit: command.limit }),
|
|
6764
|
+
offset: command.offset,
|
|
6765
|
+
};
|
|
6766
|
+
const payload = await requestDbApi({
|
|
6767
|
+
artifactId: command.artifactId,
|
|
6768
|
+
dbPath: "query",
|
|
6769
|
+
method: "POST",
|
|
6770
|
+
baseUrl: credentials.baseUrl,
|
|
6771
|
+
token: credentials.token,
|
|
6772
|
+
fetcher: runtime.fetcher ?? fetch,
|
|
6773
|
+
body,
|
|
6774
|
+
});
|
|
6775
|
+
const rows = Array.isArray(payload.rows) ? payload.rows : [];
|
|
6776
|
+
printDbPayload(command, io, payload, [
|
|
6777
|
+
`app_id: ${command.artifactId}`,
|
|
6778
|
+
`environment: ${command.environment}`,
|
|
6779
|
+
`rows: ${rows.length}`,
|
|
6780
|
+
`offset: ${String(payload.offset ?? command.offset)}`,
|
|
6781
|
+
`applied_limit: ${String(payload.appliedLimit ?? command.limit ?? "")}`,
|
|
6782
|
+
`truncated: ${String(payload.truncated ?? false)}`,
|
|
6783
|
+
...(typeof payload.nextOffset === "number"
|
|
6784
|
+
? [`next_offset: ${payload.nextOffset}`]
|
|
6785
|
+
: []),
|
|
6786
|
+
]);
|
|
6787
|
+
return 0;
|
|
6788
|
+
}
|
|
6130
6789
|
async function runAnalyticsSummary(command, io, runtime) {
|
|
6131
6790
|
const credentials = await resolveAnalyticsCredentials(command, runtime);
|
|
6132
6791
|
const payload = await requestAnalyticsApi({
|
|
@@ -6530,12 +7189,106 @@ async function runAnalyticsChartsDelete(command, io, runtime) {
|
|
|
6530
7189
|
]);
|
|
6531
7190
|
return 0;
|
|
6532
7191
|
}
|
|
7192
|
+
function cliErrorMessage(error) {
|
|
7193
|
+
if (error instanceof Error) {
|
|
7194
|
+
return error.message.split("\n")[0] ?? error.message;
|
|
7195
|
+
}
|
|
7196
|
+
return String(error);
|
|
7197
|
+
}
|
|
7198
|
+
function cliErrorCode(error) {
|
|
7199
|
+
if (error instanceof TenderCliDbApiError || error instanceof TenderCliAnalyticsApiError) {
|
|
7200
|
+
return error.reasonCode;
|
|
7201
|
+
}
|
|
7202
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
7203
|
+
const code = error.code;
|
|
7204
|
+
if (code === "ENOENT") {
|
|
7205
|
+
return "file_not_found";
|
|
7206
|
+
}
|
|
7207
|
+
if (typeof code === "string" && code.length > 0) {
|
|
7208
|
+
return code.toLowerCase();
|
|
7209
|
+
}
|
|
7210
|
+
}
|
|
7211
|
+
return error instanceof TenderCliUsageError ? "cli_usage_error" : "cli_error";
|
|
7212
|
+
}
|
|
7213
|
+
function commandNameForError(args, command) {
|
|
7214
|
+
if (command) {
|
|
7215
|
+
return command.command === "help" ? `help ${command.topic}` : command.command;
|
|
7216
|
+
}
|
|
7217
|
+
if (args[0] === "app" && args[1] === "db" && args[2]) {
|
|
7218
|
+
return `app db ${args[2]}`;
|
|
7219
|
+
}
|
|
7220
|
+
if (args[0] === "app" && args[1]) {
|
|
7221
|
+
return `app ${args[1]}`;
|
|
7222
|
+
}
|
|
7223
|
+
return args[0] ?? "unknown";
|
|
7224
|
+
}
|
|
7225
|
+
function dbPathFromCommandName(commandName) {
|
|
7226
|
+
if (commandName === "app db capabilities") {
|
|
7227
|
+
return "capabilities";
|
|
7228
|
+
}
|
|
7229
|
+
if (commandName === "app db tables") {
|
|
7230
|
+
return "tables";
|
|
7231
|
+
}
|
|
7232
|
+
if (commandName === "app db schema") {
|
|
7233
|
+
return "schema";
|
|
7234
|
+
}
|
|
7235
|
+
if (commandName === "app db query") {
|
|
7236
|
+
return "query";
|
|
7237
|
+
}
|
|
7238
|
+
return null;
|
|
7239
|
+
}
|
|
7240
|
+
function artifactIdForError(args, command) {
|
|
7241
|
+
if (command && "artifactId" in command) {
|
|
7242
|
+
return command.artifactId;
|
|
7243
|
+
}
|
|
7244
|
+
if (args[0] === "app" && args[1] === "db" && args[3] && !args[3].startsWith("-")) {
|
|
7245
|
+
return args[3];
|
|
7246
|
+
}
|
|
7247
|
+
return null;
|
|
7248
|
+
}
|
|
7249
|
+
function scopeForError(error, args, command) {
|
|
7250
|
+
if (error instanceof TenderCliDbApiError || error instanceof TenderCliAnalyticsApiError) {
|
|
7251
|
+
return error.scope;
|
|
7252
|
+
}
|
|
7253
|
+
const commandName = commandNameForError(args, command);
|
|
7254
|
+
const dbPath = dbPathFromCommandName(commandName);
|
|
7255
|
+
const artifactId = artifactIdForError(args, command);
|
|
7256
|
+
if (dbPath && artifactId) {
|
|
7257
|
+
return {
|
|
7258
|
+
artifactId,
|
|
7259
|
+
surface: `db/${dbPath}`,
|
|
7260
|
+
};
|
|
7261
|
+
}
|
|
7262
|
+
return null;
|
|
7263
|
+
}
|
|
7264
|
+
function exampleForError(error, args, command) {
|
|
7265
|
+
if (error instanceof TenderCliDbApiError || error instanceof TenderCliAnalyticsApiError) {
|
|
7266
|
+
return error.example;
|
|
7267
|
+
}
|
|
7268
|
+
const commandName = commandNameForError(args, command);
|
|
7269
|
+
const dbPath = dbPathFromCommandName(commandName);
|
|
7270
|
+
const artifactId = artifactIdForError(args, command) ?? "artifact_123";
|
|
7271
|
+
return dbPath ? dbCorrectedExample(artifactId, dbPath) : null;
|
|
7272
|
+
}
|
|
7273
|
+
function cliErrorPayload(error, args, command) {
|
|
7274
|
+
const scope = scopeForError(error, args, command);
|
|
7275
|
+
const example = exampleForError(error, args, command);
|
|
7276
|
+
return {
|
|
7277
|
+
ok: false,
|
|
7278
|
+
command: commandNameForError(args, command),
|
|
7279
|
+
reasonCode: cliErrorCode(error),
|
|
7280
|
+
message: cliErrorMessage(error),
|
|
7281
|
+
...(scope ? { scope } : {}),
|
|
7282
|
+
...(example ? { example } : {}),
|
|
7283
|
+
};
|
|
7284
|
+
}
|
|
6533
7285
|
export async function runTenderCli(args, io = {
|
|
6534
7286
|
stdout: (message) => console.log(message),
|
|
6535
7287
|
stderr: (message) => console.error(message),
|
|
6536
7288
|
}, runtime = {}) {
|
|
7289
|
+
let command = null;
|
|
6537
7290
|
try {
|
|
6538
|
-
|
|
7291
|
+
command = parseTenderArgs(args);
|
|
6539
7292
|
if (command.command === "help") {
|
|
6540
7293
|
if (command.topic === "capabilities") {
|
|
6541
7294
|
io.stdout(capabilitiesHelp());
|
|
@@ -6660,6 +7413,21 @@ export async function runTenderCli(args, io = {
|
|
|
6660
7413
|
else if (command.topic === "app analytics properties activate") {
|
|
6661
7414
|
io.stdout(analyticsPropertiesActivateHelp());
|
|
6662
7415
|
}
|
|
7416
|
+
else if (command.topic === "app db") {
|
|
7417
|
+
io.stdout(dbHelp());
|
|
7418
|
+
}
|
|
7419
|
+
else if (command.topic === "app db capabilities") {
|
|
7420
|
+
io.stdout(dbCapabilitiesHelp());
|
|
7421
|
+
}
|
|
7422
|
+
else if (command.topic === "app db tables") {
|
|
7423
|
+
io.stdout(dbTablesHelp());
|
|
7424
|
+
}
|
|
7425
|
+
else if (command.topic === "app db schema") {
|
|
7426
|
+
io.stdout(dbSchemaHelp());
|
|
7427
|
+
}
|
|
7428
|
+
else if (command.topic === "app db query") {
|
|
7429
|
+
io.stdout(dbQueryHelp());
|
|
7430
|
+
}
|
|
6663
7431
|
else if (command.topic === "app") {
|
|
6664
7432
|
io.stdout(appHelp());
|
|
6665
7433
|
}
|
|
@@ -6776,22 +7544,44 @@ export async function runTenderCli(args, io = {
|
|
|
6776
7544
|
if (command.command === "app analytics properties activate") {
|
|
6777
7545
|
return await runAnalyticsPropertiesActivate(command, io, runtime);
|
|
6778
7546
|
}
|
|
7547
|
+
if (command.command === "app db capabilities") {
|
|
7548
|
+
return await runDbCapabilities(command, io, runtime);
|
|
7549
|
+
}
|
|
7550
|
+
if (command.command === "app db tables") {
|
|
7551
|
+
return await runDbTables(command, io, runtime);
|
|
7552
|
+
}
|
|
7553
|
+
if (command.command === "app db schema") {
|
|
7554
|
+
return await runDbSchema(command, io, runtime);
|
|
7555
|
+
}
|
|
7556
|
+
if (command.command === "app db query") {
|
|
7557
|
+
return await runDbQuery(command, io, runtime);
|
|
7558
|
+
}
|
|
6779
7559
|
if (command.command === "app git remote") {
|
|
6780
7560
|
return await runGitRemote(command, io, runtime);
|
|
6781
7561
|
}
|
|
6782
7562
|
return await runDoctor(command, io);
|
|
6783
7563
|
}
|
|
6784
7564
|
catch (error) {
|
|
7565
|
+
if (args.includes("--json") && args[0] === "app" && args[1] === "db") {
|
|
7566
|
+
io.stdout(JSON.stringify(cliErrorPayload(error, args, command), null, 2));
|
|
7567
|
+
return error instanceof TenderCliUsageError ? 2 : 1;
|
|
7568
|
+
}
|
|
6785
7569
|
io.stderr(error instanceof Error ? error.message : String(error));
|
|
6786
7570
|
io.stderr("Run `tender --help` for examples.");
|
|
6787
7571
|
return error instanceof TenderCliUsageError ? 2 : 1;
|
|
6788
7572
|
}
|
|
6789
7573
|
}
|
|
6790
7574
|
const currentFile = fileURLToPath(import.meta.url);
|
|
7575
|
+
function hasFlagValue(args, flag, value) {
|
|
7576
|
+
return args.some((arg, index) => (arg === flag && args[index + 1] === value) || arg === `${flag}=${value}`);
|
|
7577
|
+
}
|
|
6791
7578
|
function shouldReadEntrypointStdin(args) {
|
|
6792
7579
|
return (args[0] === "app" &&
|
|
6793
7580
|
((args[1] === "git" && args[2] === "credential") ||
|
|
6794
|
-
(args[1] === "analytics" && args
|
|
7581
|
+
(args[1] === "analytics" && hasFlagValue(args, "--spec", "-")) ||
|
|
7582
|
+
(args[1] === "db" &&
|
|
7583
|
+
args[2] === "query" &&
|
|
7584
|
+
hasFlagValue(args, "--file", "-"))));
|
|
6795
7585
|
}
|
|
6796
7586
|
async function readEntrypointStdin() {
|
|
6797
7587
|
if (process.stdin.isTTY) {
|