@tenderprompt/cli 0.1.12 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +60 -0
  2. package/dist/index.js +575 -9
  3. 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,10 @@ 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 --json
137
+ tender app db schema artifact_123 --table votes --json
138
+ tender app db query artifact_123 --sql "select * from votes limit 20" --json
131
139
  ```
132
140
 
133
141
  ## Analytics
@@ -226,6 +234,56 @@ filter. `analytics query` is read-only, so use
226
234
  query fails for a missing dashboard-query property, the CLI prints the exact
227
235
  activation commands to run and does not perform that write automatically.
228
236
 
237
+ Event payload properties can be tracked by the app, observed in bounded catalog
238
+ samples, and still not be queryable in fast dashboard charts. Register intended
239
+ dashboard dimensions and metrics before meaningful traffic starts. Use
240
+ dimensions for grouping values such as variant, plan, step, source, or outcome,
241
+ and metrics for numeric values such as score, amount, duration, or quantity.
242
+ Making a property queryable does not require an app redeploy, but older
243
+ dashboard-query rows may not fully populate grouped charts until new traffic
244
+ arrives. Do not make every property queryable; avoid PII, secrets, free text,
245
+ ids, URLs, or high-cardinality values unless the chart has a clear purpose.
246
+
247
+ ## App Database
248
+
249
+ App database commands are artifact-scoped and go through Tender's server-side
250
+ authorization and read-only query checks. The CLI only accepts an app id; it
251
+ does not accept tenant ids, runner names, provider account ids, or runtime
252
+ storage identifiers.
253
+
254
+ Start with capabilities:
255
+
256
+ ```bash
257
+ tender auth login --device --profile db-read --artifact artifact_123 --ttl 7d --save-profile db --json
258
+
259
+ tender app db capabilities artifact_123 --profile db --json
260
+ ```
261
+
262
+ Common read-only commands:
263
+
264
+ ```bash
265
+ tender app db tables artifact_123 --profile db --json
266
+
267
+ tender app db schema artifact_123 --table votes --profile db --json
268
+
269
+ tender app db query artifact_123 --sql "select * from votes limit 20" --profile db --json
270
+
271
+ tender app db query artifact_123 \
272
+ --sql "select * from votes where choice = ?" \
273
+ --param coffee \
274
+ --profile db \
275
+ --json
276
+
277
+ tender app db query artifact_123 --file ./debug.sql --limit 50 --profile db --json
278
+
279
+ tender app db query artifact_123 --file - --profile db --json < debug.sql
280
+ ```
281
+
282
+ The query command sends one read-only statement with positional bindings.
283
+ Client and server limits include a 100 KB statement cap and at most 100 bound
284
+ parameters. The server enforces the final policy, row limits, response limits,
285
+ artifact scope, and audit logging.
286
+
229
287
  ## Safety Model
230
288
 
231
289
  - Local agents run in the user's environment, not in Tender.
@@ -242,6 +300,8 @@ activation commands to run and does not perform that write automatically.
242
300
  tenant/artifact-scoped.
243
301
  - Raw analytics event export is intentionally not available; exports are bounded
244
302
  catalog metadata or validated aggregate query rows.
303
+ - App database commands require `db:read`, should use artifact-scoped tokens,
304
+ and never require provider account ids or runtime storage identifiers.
245
305
 
246
306
  ## Related Package
247
307
 
package/dist/index.js CHANGED
@@ -43,14 +43,24 @@ class TenderCliAnalyticsApiError extends Error {
43
43
  reasonCode;
44
44
  constructor(message, reasonCode) {
45
45
  super(message);
46
- this.reasonCode = reasonCode;
47
46
  this.name = "TenderCliAnalyticsApiError";
47
+ this.reasonCode = reasonCode;
48
+ }
49
+ }
50
+ class TenderCliDbApiError extends Error {
51
+ reasonCode;
52
+ constructor(message, reasonCode) {
53
+ super(message);
54
+ this.name = "TenderCliDbApiError";
55
+ this.reasonCode = reasonCode;
48
56
  }
49
57
  }
50
58
  const DEFAULT_PROFILE_NAME = "default";
51
59
  const DEFAULT_BASE_URL = "https://app.tenderprompt.com";
52
60
  const ARTIFACT_SOURCE_EXPORT_MAX_FILES = 1000;
53
61
  const ARTIFACT_SOURCE_EXPORT_MAX_FILE_BYTES = 2 * 1024 * 1024;
62
+ const DB_SQL_STATEMENT_MAX_BYTES = 100 * 1024;
63
+ const DB_MAX_BOUND_PARAMETERS = 100;
54
64
  function rootHelp() {
55
65
  return `Usage: tender <command> [options]
56
66
 
@@ -89,6 +99,10 @@ Commands:
89
99
  Run a server-validated chart spec
90
100
  tender app analytics suggestions <app-id>
91
101
  Generate useful starter chart suggestions
102
+ tender app db capabilities <app-id>
103
+ Describe app database query limits and policy
104
+ tender app db query <app-id> --sql <sql>
105
+ Run one read-only app database query
92
106
  tender app context fetch <app-id>
93
107
  Fetch managed agent context into a checkout
94
108
  tender app context status <app-id>
@@ -120,6 +134,10 @@ Examples:
120
134
  tender app analytics summary artifact_123 --range 30d --json
121
135
  tender app analytics query artifact_123 --spec chart.json --json
122
136
  tender app analytics suggestions artifact_123 --range 30d --json
137
+ tender app db capabilities artifact_123 --json
138
+ tender app db tables artifact_123 --json
139
+ tender app db schema artifact_123 --table votes --json
140
+ tender app db query artifact_123 --sql "select * from votes limit 20" --json
123
141
  tender app context fetch artifact_123 --dir ./widget --dry-run --json
124
142
  tender app context status artifact_123 --dir ./widget --json
125
143
  tender app context refresh artifact_123 --dir ./widget --dry-run --json
@@ -142,12 +160,12 @@ Examples:
142
160
  tender auth status --json`;
143
161
  }
144
162
  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]
163
+ 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
164
 
147
165
  Options:
148
166
  --device Use the OAuth device-code flow.
149
167
  --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 create. Defaults to edit-preview.
168
+ --profile <value> Token permission profile: edit-preview, publish, create, or db-read. Defaults to edit-preview.
151
169
  --artifact <artifact-id> Scope the token to one artifact. Required for normal edit-preview agent work.
152
170
  --ttl <value> Token lifetime: 7d, 14d, or 30d. Defaults to 7d.
153
171
  --save-profile <name> Local config profile name. Defaults to default.
@@ -157,7 +175,8 @@ Options:
157
175
  Examples:
158
176
  tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d
159
177
  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`;
178
+ tender auth login --device --profile create --ttl 7d --save-profile account
179
+ tender auth login --device --profile db-read --artifact artifact_123 --ttl 7d --save-profile db`;
161
180
  }
162
181
  function authStatusHelp() {
163
182
  return `Usage: tender auth status [--profile <name>] [--json]
@@ -217,6 +236,14 @@ Commands:
217
236
  List properties available to dashboard queries
218
237
  tender app analytics properties activate <app-id>
219
238
  Make a custom event property queryable
239
+ tender app db capabilities <app-id>
240
+ Describe app database query limits and policy
241
+ tender app db tables <app-id>
242
+ List app database tables and views
243
+ tender app db schema <app-id> --table <name>
244
+ Show table and index schema for one table
245
+ tender app db query <app-id> --sql <sql>
246
+ Run one read-only app database query
220
247
  tender app context fetch <app-id>
221
248
  Fetch AGENTS.md, skills, and Tender App scaffold files
222
249
  tender app context status <app-id>
@@ -257,6 +284,10 @@ Examples:
257
284
  tender app analytics charts delete artifact_123 --chart analytics_chart_123 --dry-run --json
258
285
  tender app analytics properties list artifact_123 --json
259
286
  tender app analytics properties activate artifact_123 --event signup_started --property plan --type dimension --dry-run --json
287
+ tender app db capabilities artifact_123 --json
288
+ tender app db tables artifact_123 --json
289
+ tender app db schema artifact_123 --table votes --json
290
+ tender app db query artifact_123 --sql "select * from votes limit 20" --json
260
291
  tender app context fetch artifact_123 --dir ./widget --dry-run --json
261
292
  tender app context status artifact_123 --dir ./widget --json
262
293
  tender app context refresh artifact_123 --dir ./widget --dry-run --json
@@ -435,6 +466,57 @@ Examples:
435
466
  tender app analytics properties activate artifact_123 --event signup_started --property plan --type dimension --dry-run --json
436
467
  tender app analytics properties activate artifact_123 --event score_saved --property score --type metric --json`;
437
468
  }
469
+ function dbHelp() {
470
+ return `Usage: tender app db <command> <app-id> [options]
471
+
472
+ Commands:
473
+ tender app db capabilities <app-id> Describe app database policy and limits
474
+ tender app db tables <app-id> List app database tables and views
475
+ tender app db schema <app-id> Show schema for one table
476
+ tender app db query <app-id> Run one read-only SQL query
477
+
478
+ Examples:
479
+ tender app db capabilities artifact_123 --json
480
+ tender app db tables artifact_123 --json
481
+ tender app db schema artifact_123 --table votes --json
482
+ tender app db query artifact_123 --sql "select * from votes limit 20" --json
483
+ tender app db query artifact_123 --file ./debug.sql --param active --limit 50 --json`;
484
+ }
485
+ function dbCapabilitiesHelp() {
486
+ return `Usage: tender app db capabilities <app-id> [--base-url <url>] [--token <token>] [--profile <name>] [--json]
487
+
488
+ Examples:
489
+ tender app db capabilities artifact_123 --json`;
490
+ }
491
+ function dbTablesHelp() {
492
+ return `Usage: tender app db tables <app-id> [--env preview] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
493
+
494
+ Examples:
495
+ tender app db tables artifact_123 --json
496
+ tender app db tables artifact_123 --env preview --json`;
497
+ }
498
+ function dbSchemaHelp() {
499
+ return `Usage: tender app db schema <app-id> --table <name> [--env preview] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
500
+
501
+ Examples:
502
+ tender app db schema artifact_123 --table votes --json
503
+ tender app db schema artifact_123 --table preference_votes --env preview --json`;
504
+ }
505
+ function dbQueryHelp() {
506
+ return `Usage: tender app db query <app-id> (--sql <sql> | --file <path|->) [--param <value>] [--limit <n>] [--env preview] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
507
+
508
+ Options:
509
+ --sql <sql> SQL statement. Must be one read-only statement.
510
+ --file <path|-> Read SQL from a file or stdin with -.
511
+ --param <value> Bind one positional parameter. Repeat for multiple params. Values parse as JSON scalars, falling back to string.
512
+ --limit <n> Interactive row limit. Server caps the maximum.
513
+
514
+ Examples:
515
+ tender app db query artifact_123 --sql "select * from votes limit 20" --json
516
+ tender app db query artifact_123 --sql "select * from votes where choice = ?" --param coffee --json
517
+ tender app db query artifact_123 --file ./debug.sql --limit 50 --json
518
+ tender app db query artifact_123 --file - --json < debug.sql`;
519
+ }
438
520
  function artifactsListHelp() {
439
521
  return `Usage: tender app list [--base-url <url>] [--token <token>] [--profile <name>] [--json]
440
522
 
@@ -767,6 +849,7 @@ function tenderCliCapabilities() {
767
849
  "tender auth --help",
768
850
  "tender app --help",
769
851
  "tender app analytics --help",
852
+ "tender app db --help",
770
853
  "tender app bindings outbound --help",
771
854
  ],
772
855
  },
@@ -832,6 +915,23 @@ function tenderCliCapabilities() {
832
915
  "Use list before writes so agents can avoid duplicates and explain the exact chart they are changing.",
833
916
  ],
834
917
  },
918
+ {
919
+ name: "app_database_inspection",
920
+ when: "When inspecting app-local database tables from a scoped token.",
921
+ commands: [
922
+ "tender auth login --device --profile db-read --artifact <artifact-id> --ttl 7d --save-profile db --json",
923
+ "tender app db capabilities <artifact-id> --profile db --json",
924
+ "tender app db tables <artifact-id> --profile db --json",
925
+ "tender app db schema <artifact-id> --table <table-name> --profile db --json",
926
+ "tender app db query <artifact-id> --sql \"select * from <table-name> limit 20\" --profile db --json",
927
+ "tender app db query <artifact-id> --file ./debug.sql --profile db --json",
928
+ ],
929
+ notes: [
930
+ "The CLI only accepts artifact ids. It never accepts tenant ids, runner names, provider account ids, or runtime storage identifiers.",
931
+ "Read-only policy is enforced by the server before the runtime shim executes SQL.",
932
+ "Use --file - to pipe SQL from stdin without prompts.",
933
+ ],
934
+ },
835
935
  {
836
936
  name: "outbound_http_binding",
837
937
  when: "When app code needs external APIs, OAuth/token exchange, webhooks, provider SDKs, or secrets.",
@@ -925,6 +1025,30 @@ function tenderCliCapabilities() {
925
1025
  writes: true,
926
1026
  dryRun: true,
927
1027
  },
1028
+ {
1029
+ command: "tender app db capabilities <artifact-id> --json",
1030
+ purpose: "Inspect app database policy, limits, and supported capabilities for one artifact.",
1031
+ requiresAuth: true,
1032
+ writes: false,
1033
+ },
1034
+ {
1035
+ command: "tender app db tables <artifact-id> --json",
1036
+ purpose: "List app database tables and views for one artifact.",
1037
+ requiresAuth: true,
1038
+ writes: false,
1039
+ },
1040
+ {
1041
+ command: "tender app db schema <artifact-id> --table <table-name> --json",
1042
+ purpose: "Inspect table, index, and trigger schema for one app database table.",
1043
+ requiresAuth: true,
1044
+ writes: false,
1045
+ },
1046
+ {
1047
+ command: "tender app db query <artifact-id> --sql \"select * from <table-name> limit 20\" --json",
1048
+ purpose: "Run one bounded read-only app database query.",
1049
+ requiresAuth: true,
1050
+ writes: false,
1051
+ },
928
1052
  ],
929
1053
  security: [
930
1054
  "Prefer artifact-scoped device tokens.",
@@ -932,6 +1056,7 @@ function tenderCliCapabilities() {
932
1056
  "Do not call Tender HTTP APIs directly from agent workflows.",
933
1057
  "Do not put third-party fetch calls in src/server.ts; use outbound bindings.",
934
1058
  "Do not request direct query credentials or raw SQL for analytics.",
1059
+ "Do not use tenant ids, runner names, provider account ids, or runtime storage identifiers for app database inspection.",
935
1060
  ],
936
1061
  };
937
1062
  }
@@ -2264,10 +2389,11 @@ function parseContextRefreshArgs(args) {
2264
2389
  function parseTokenProfile(value) {
2265
2390
  if (value === "edit-preview" ||
2266
2391
  value === "publish" ||
2267
- value === "create") {
2392
+ value === "create" ||
2393
+ value === "db-read") {
2268
2394
  return value;
2269
2395
  }
2270
- throw new TenderCliUsageError("--profile must be one of edit-preview, publish, or create.");
2396
+ throw new TenderCliUsageError("--profile must be one of edit-preview, publish, create, or db-read.");
2271
2397
  }
2272
2398
  function parsePositiveIntegerFlag(value, flag) {
2273
2399
  const parsed = Number(value);
@@ -2341,6 +2467,9 @@ function parseAuthLoginArgs(args) {
2341
2467
  if (tokenProfile === "create" && artifactId) {
2342
2468
  throw new TenderCliUsageError("--profile create must be account-scoped; remove --artifact.");
2343
2469
  }
2470
+ if (tokenProfile === "db-read" && !artifactId) {
2471
+ throw new TenderCliUsageError("--profile db-read requires --artifact <artifact-id>.");
2472
+ }
2344
2473
  return {
2345
2474
  command: "auth login",
2346
2475
  baseUrl,
@@ -2941,10 +3070,12 @@ function analyticsChartTemplates() {
2941
3070
  purpose: "Compare a queryable dimension property observed in events.",
2942
3071
  usableAsIs: false,
2943
3072
  placeholders: {
3073
+ eventName: "<event_name>",
2944
3074
  property: "<queryable_dimension_property>",
2945
3075
  },
2946
3076
  adaptWith: [
2947
3077
  "Use a queryable property key from capabilities chartSpec.groupBy, such as property:plan.",
3078
+ "Choose the event that owns the property and keep the event_name filter.",
2948
3079
  "Choose stable categorical properties with enough observations to compare.",
2949
3080
  ],
2950
3081
  spec: {
@@ -2953,7 +3084,7 @@ function analyticsChartTemplates() {
2953
3084
  visualization: "bar",
2954
3085
  metric: { name: "custom_event_count" },
2955
3086
  groupBy: [{ field: "property:<queryable_dimension_property>" }],
2956
- filters: [],
3087
+ filters: [{ field: "event_name", op: "=", value: "<event_name>" }],
2957
3088
  range: { preset: "30d" },
2958
3089
  sort: [{ field: "value", direction: "desc" }],
2959
3090
  limit: 10,
@@ -2966,10 +3097,12 @@ function analyticsChartTemplates() {
2966
3097
  purpose: "Track an observed numeric event property over time.",
2967
3098
  usableAsIs: false,
2968
3099
  placeholders: {
3100
+ eventName: "<event_name>",
2969
3101
  metric: "avg:<queryable_metric_property>",
2970
3102
  },
2971
3103
  adaptWith: [
2972
3104
  "Use a queryable numeric property key from capabilities chartSpec.metrics.",
3105
+ "Choose the event that owns the property and keep the event_name filter.",
2973
3106
  "Prefer averages for scores, durations, quantities, or ordinal values.",
2974
3107
  ],
2975
3108
  spec: {
@@ -2978,7 +3111,7 @@ function analyticsChartTemplates() {
2978
3111
  visualization: "line",
2979
3112
  metric: { name: "avg:<queryable_metric_property>" },
2980
3113
  groupBy: [{ field: "day" }],
2981
- filters: [],
3114
+ filters: [{ field: "event_name", op: "=", value: "<event_name>" }],
2982
3115
  range: { preset: "30d" },
2983
3116
  sort: [{ field: "label", direction: "asc" }],
2984
3117
  limit: 30,
@@ -3292,6 +3425,211 @@ function parseAnalyticsPropertiesActivateArgs(args) {
3292
3425
  ...parsed,
3293
3426
  };
3294
3427
  }
3428
+ function parseDbCommonArgs(args, startIndex) {
3429
+ let baseUrl = null;
3430
+ let flagToken = null;
3431
+ let profileName = null;
3432
+ let json = false;
3433
+ const remaining = [];
3434
+ for (let index = startIndex; index < args.length; index += 1) {
3435
+ const arg = args[index];
3436
+ if (arg === "--json") {
3437
+ json = true;
3438
+ continue;
3439
+ }
3440
+ if (arg === "--base-url") {
3441
+ baseUrl = parseBaseUrlFlag(args, index);
3442
+ index += 1;
3443
+ continue;
3444
+ }
3445
+ if (arg === "--token") {
3446
+ flagToken = parseTokenFlag(args, index);
3447
+ index += 1;
3448
+ continue;
3449
+ }
3450
+ if (arg === "--profile") {
3451
+ profileName = parseProfileName(args[index + 1], "--profile");
3452
+ index += 1;
3453
+ continue;
3454
+ }
3455
+ remaining.push(arg);
3456
+ }
3457
+ return { baseUrl, flagToken, profileName, json, remaining };
3458
+ }
3459
+ function parseDbEnvironment(value) {
3460
+ if (value === undefined || value === "preview") {
3461
+ return "preview";
3462
+ }
3463
+ throw new TenderCliUsageError("--env must be preview.");
3464
+ }
3465
+ function parseDbFlagValue(value, flag) {
3466
+ if (!value || value.startsWith("--")) {
3467
+ throw new TenderCliUsageError(`${flag} requires a value.`);
3468
+ }
3469
+ return value;
3470
+ }
3471
+ function parseDbFilePath(value) {
3472
+ if (!value || (value !== "-" && value.startsWith("-"))) {
3473
+ throw new TenderCliUsageError("--file requires a SQL file path or - for stdin.");
3474
+ }
3475
+ return value;
3476
+ }
3477
+ function parseDbParameter(value) {
3478
+ const raw = parseDbFlagValue(value, "--param");
3479
+ try {
3480
+ const parsed = JSON.parse(raw);
3481
+ if (parsed === null ||
3482
+ typeof parsed === "string" ||
3483
+ typeof parsed === "number") {
3484
+ return parsed;
3485
+ }
3486
+ }
3487
+ catch {
3488
+ // Fall back to a string binding.
3489
+ }
3490
+ return raw;
3491
+ }
3492
+ function parseDbCapabilitiesArgs(args) {
3493
+ if (args.includes("--help") || args.includes("-h")) {
3494
+ return { command: "help", topic: "app db capabilities" };
3495
+ }
3496
+ const artifactId = args[0];
3497
+ if (!artifactId || artifactId.startsWith("-")) {
3498
+ throw new TenderCliUsageError("app id is required. Example: tender app db capabilities <app-id> --json");
3499
+ }
3500
+ const parsed = parseDbCommonArgs(args, 1);
3501
+ if (parsed.remaining.length > 0) {
3502
+ throw new TenderCliUsageError(`Unknown app db capabilities option: ${parsed.remaining[0]}`);
3503
+ }
3504
+ return { command: "app db capabilities", artifactId, ...parsed };
3505
+ }
3506
+ function parseDbTablesArgs(args) {
3507
+ if (args.includes("--help") || args.includes("-h")) {
3508
+ return { command: "help", topic: "app db tables" };
3509
+ }
3510
+ const artifactId = args[0];
3511
+ if (!artifactId || artifactId.startsWith("-")) {
3512
+ throw new TenderCliUsageError("app id is required. Example: tender app db tables <app-id> --json");
3513
+ }
3514
+ const parsed = parseDbCommonArgs(args, 1);
3515
+ let environment = "preview";
3516
+ for (let index = 0; index < parsed.remaining.length; index += 1) {
3517
+ const arg = parsed.remaining[index];
3518
+ if (arg === "--env") {
3519
+ environment = parseDbEnvironment(parsed.remaining[index + 1]);
3520
+ index += 1;
3521
+ continue;
3522
+ }
3523
+ throw new TenderCliUsageError(`Unknown app db tables option: ${arg}`);
3524
+ }
3525
+ return { command: "app db tables", artifactId, environment, ...parsed };
3526
+ }
3527
+ function parseDbSchemaArgs(args) {
3528
+ if (args.includes("--help") || args.includes("-h")) {
3529
+ return { command: "help", topic: "app db schema" };
3530
+ }
3531
+ const artifactId = args[0];
3532
+ if (!artifactId || artifactId.startsWith("-")) {
3533
+ throw new TenderCliUsageError("app id is required. Example: tender app db schema <app-id> --table votes --json");
3534
+ }
3535
+ const parsed = parseDbCommonArgs(args, 1);
3536
+ let environment = "preview";
3537
+ let tableName = null;
3538
+ for (let index = 0; index < parsed.remaining.length; index += 1) {
3539
+ const arg = parsed.remaining[index];
3540
+ if (arg === "--env") {
3541
+ environment = parseDbEnvironment(parsed.remaining[index + 1]);
3542
+ index += 1;
3543
+ continue;
3544
+ }
3545
+ if (arg === "--table") {
3546
+ tableName = parseRequiredFlagValue(parsed.remaining[index + 1], "--table");
3547
+ index += 1;
3548
+ continue;
3549
+ }
3550
+ throw new TenderCliUsageError(`Unknown app db schema option: ${arg}`);
3551
+ }
3552
+ if (!tableName) {
3553
+ throw new TenderCliUsageError("--table is required. Example: tender app db schema artifact_123 --table votes --json");
3554
+ }
3555
+ return {
3556
+ command: "app db schema",
3557
+ artifactId,
3558
+ environment,
3559
+ tableName,
3560
+ ...parsed,
3561
+ };
3562
+ }
3563
+ function parseDbQueryArgs(args) {
3564
+ if (args.includes("--help") || args.includes("-h")) {
3565
+ return { command: "help", topic: "app db query" };
3566
+ }
3567
+ const artifactId = args[0];
3568
+ if (!artifactId || artifactId.startsWith("-")) {
3569
+ throw new TenderCliUsageError("app id is required. Example: tender app db query <app-id> --sql \"select * from votes limit 20\" --json");
3570
+ }
3571
+ const parsed = parseDbCommonArgs(args, 1);
3572
+ let environment = "preview";
3573
+ let sqlSource = null;
3574
+ const params = [];
3575
+ let limit = null;
3576
+ for (let index = 0; index < parsed.remaining.length; index += 1) {
3577
+ const arg = parsed.remaining[index];
3578
+ if (arg === "--env") {
3579
+ environment = parseDbEnvironment(parsed.remaining[index + 1]);
3580
+ index += 1;
3581
+ continue;
3582
+ }
3583
+ if (arg === "--sql") {
3584
+ if (sqlSource) {
3585
+ throw new TenderCliUsageError("--sql and --file cannot be repeated or combined.");
3586
+ }
3587
+ sqlSource = {
3588
+ kind: "inline",
3589
+ value: parseDbFlagValue(parsed.remaining[index + 1], "--sql"),
3590
+ };
3591
+ index += 1;
3592
+ continue;
3593
+ }
3594
+ if (arg === "--file") {
3595
+ if (sqlSource) {
3596
+ throw new TenderCliUsageError("--sql and --file cannot be repeated or combined.");
3597
+ }
3598
+ sqlSource = {
3599
+ kind: "file",
3600
+ value: parseDbFilePath(parsed.remaining[index + 1]),
3601
+ };
3602
+ index += 1;
3603
+ continue;
3604
+ }
3605
+ if (arg === "--param") {
3606
+ params.push(parseDbParameter(parsed.remaining[index + 1]));
3607
+ index += 1;
3608
+ continue;
3609
+ }
3610
+ if (arg === "--limit") {
3611
+ limit = parsePositiveIntegerFlag(parsed.remaining[index + 1], "--limit");
3612
+ index += 1;
3613
+ continue;
3614
+ }
3615
+ throw new TenderCliUsageError(`Unknown app db query option: ${arg}`);
3616
+ }
3617
+ if (!sqlSource) {
3618
+ throw new TenderCliUsageError("--sql or --file is required. Example: tender app db query artifact_123 --sql \"select * from votes limit 20\" --json");
3619
+ }
3620
+ if (params.length > DB_MAX_BOUND_PARAMETERS) {
3621
+ throw new TenderCliUsageError(`App database queries are limited to ${DB_MAX_BOUND_PARAMETERS} bound parameters.`);
3622
+ }
3623
+ return {
3624
+ command: "app db query",
3625
+ artifactId,
3626
+ environment,
3627
+ sqlSource,
3628
+ params,
3629
+ limit,
3630
+ ...parsed,
3631
+ };
3632
+ }
3295
3633
  function parseTenderArgs(args) {
3296
3634
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
3297
3635
  return { command: "help", topic: "root" };
@@ -3441,6 +3779,27 @@ function parseTenderArgs(args) {
3441
3779
  resourceArgs[3] === "delete") {
3442
3780
  return parseAnalyticsChartsDeleteArgs(resourceArgs.slice(4));
3443
3781
  }
3782
+ if (resourceArgs[1] === "db" &&
3783
+ (resourceArgs[2] === undefined ||
3784
+ resourceArgs[2] === "--help" ||
3785
+ resourceArgs[2] === "-h")) {
3786
+ return { command: "help", topic: "app db" };
3787
+ }
3788
+ if (resourceArgs[1] === "db" && resourceArgs[2] === "capabilities") {
3789
+ return parseDbCapabilitiesArgs(resourceArgs.slice(3));
3790
+ }
3791
+ if (resourceArgs[1] === "db" && resourceArgs[2] === "tables") {
3792
+ return parseDbTablesArgs(resourceArgs.slice(3));
3793
+ }
3794
+ if (resourceArgs[1] === "db" && resourceArgs[2] === "schema") {
3795
+ return parseDbSchemaArgs(resourceArgs.slice(3));
3796
+ }
3797
+ if (resourceArgs[1] === "db" && resourceArgs[2] === "query") {
3798
+ return parseDbQueryArgs(resourceArgs.slice(3));
3799
+ }
3800
+ if (resourceArgs[1] === "db") {
3801
+ throw new TenderCliUsageError(`Unknown app db command: ${resourceArgs[2]}`);
3802
+ }
3444
3803
  if (resourceArgs[1] === "context" &&
3445
3804
  (resourceArgs[2] === undefined ||
3446
3805
  resourceArgs[2] === "--help" ||
@@ -3842,6 +4201,55 @@ async function requestAnalyticsApi(input) {
3842
4201
  }
3843
4202
  return payload;
3844
4203
  }
4204
+ function appendDbEnvironment(params, environment) {
4205
+ params.set("env", environment);
4206
+ }
4207
+ async function requestDbApi(input) {
4208
+ const params = new URLSearchParams();
4209
+ if (input.environment) {
4210
+ appendDbEnvironment(params, input.environment);
4211
+ }
4212
+ if (input.tableName) {
4213
+ params.set("table", input.tableName);
4214
+ }
4215
+ const query = params.size > 0 ? `?${params.toString()}` : "";
4216
+ const response = await input.fetcher(`${input.baseUrl}/api/v1/artifacts/${encodeURIComponent(input.artifactId)}/db/${input.dbPath}${query}`, {
4217
+ method: input.method,
4218
+ headers: {
4219
+ authorization: `Bearer ${input.token}`,
4220
+ accept: "application/json",
4221
+ ...(input.body === undefined ? {} : { "content-type": "application/json" }),
4222
+ },
4223
+ body: input.body === undefined ? undefined : JSON.stringify(input.body),
4224
+ });
4225
+ const payload = await parseJsonResponse(response);
4226
+ if (!response.ok || !payload?.ok) {
4227
+ const reasonCode = (typeof payload?.reasonCode === "string" && payload.reasonCode) ||
4228
+ `http_${response.status}`;
4229
+ const message = (typeof payload?.message === "string" && payload.message) ||
4230
+ (typeof payload?.reasonCode === "string" && payload.reasonCode) ||
4231
+ `App database request failed with HTTP ${response.status}.`;
4232
+ throw new TenderCliDbApiError([
4233
+ message,
4234
+ `Scope: artifact ${input.artifactId}, db/${input.dbPath}.`,
4235
+ `Reason: ${reasonCode}.`,
4236
+ `Example: ${dbCorrectedExample(input.artifactId, input.dbPath)}.`,
4237
+ ].join("\n"), reasonCode);
4238
+ }
4239
+ return payload;
4240
+ }
4241
+ function dbCorrectedExample(artifactId, dbPath) {
4242
+ if (dbPath === "capabilities") {
4243
+ return `tender app db capabilities ${artifactId} --json`;
4244
+ }
4245
+ if (dbPath === "tables") {
4246
+ return `tender app db tables ${artifactId} --json`;
4247
+ }
4248
+ if (dbPath === "schema") {
4249
+ return `tender app db schema ${artifactId} --table <table-name> --json`;
4250
+ }
4251
+ return `tender app db query ${artifactId} --sql "select * from <table-name> limit 20" --json`;
4252
+ }
3845
4253
  function analyticsCorrectedExample(artifactId, analyticsPath) {
3846
4254
  if (analyticsPath === "capabilities") {
3847
4255
  return `tender app analytics capabilities ${artifactId} --include-catalog --range 30d --json`;
@@ -6127,6 +6535,131 @@ async function resolveAnalyticsCredentials(command, runtime) {
6127
6535
  env: runtime.env ?? process.env,
6128
6536
  });
6129
6537
  }
6538
+ async function resolveDbCredentials(command, runtime) {
6539
+ return await resolveApiCredentials({
6540
+ baseUrl: command.baseUrl,
6541
+ flagToken: command.flagToken,
6542
+ profileName: command.profileName,
6543
+ env: runtime.env ?? process.env,
6544
+ });
6545
+ }
6546
+ function validateDbSql(sql) {
6547
+ if (!sql.trim()) {
6548
+ throw new TenderCliUsageError("SQL statement is required.");
6549
+ }
6550
+ const byteLength = new TextEncoder().encode(sql).byteLength;
6551
+ if (byteLength > DB_SQL_STATEMENT_MAX_BYTES) {
6552
+ throw new TenderCliUsageError(`SQL statement is limited to ${DB_SQL_STATEMENT_MAX_BYTES} bytes.`);
6553
+ }
6554
+ }
6555
+ async function readDbSql(command, runtime) {
6556
+ const sql = command.sqlSource.kind === "inline"
6557
+ ? command.sqlSource.value
6558
+ : command.sqlSource.value === "-"
6559
+ ? runtime.stdin
6560
+ : await readFile(path.resolve(command.sqlSource.value), "utf8");
6561
+ if (!sql) {
6562
+ throw new TenderCliUsageError("--file - requires SQL on stdin.");
6563
+ }
6564
+ validateDbSql(sql);
6565
+ return sql;
6566
+ }
6567
+ function printDbPayload(command, io, payload, fallbackLines) {
6568
+ if (command.json) {
6569
+ io.stdout(JSON.stringify(payload, null, 2));
6570
+ }
6571
+ else {
6572
+ io.stdout(fallbackLines.join("\n"));
6573
+ }
6574
+ }
6575
+ async function runDbCapabilities(command, io, runtime) {
6576
+ const credentials = await resolveDbCredentials(command, runtime);
6577
+ const payload = await requestDbApi({
6578
+ artifactId: command.artifactId,
6579
+ dbPath: "capabilities",
6580
+ method: "GET",
6581
+ baseUrl: credentials.baseUrl,
6582
+ token: credentials.token,
6583
+ fetcher: runtime.fetcher ?? fetch,
6584
+ });
6585
+ const limits = isCliJsonRecord(payload.limits) ? payload.limits : {};
6586
+ const runtimeInfo = isCliJsonRecord(payload.runtime) ? payload.runtime : {};
6587
+ printDbPayload(command, io, payload, [
6588
+ `app_id: ${command.artifactId}`,
6589
+ `backend: ${String(runtimeInfo.backend ?? "app-database")}`,
6590
+ `default_limit: ${String(limits.defaultLimit ?? "")}`,
6591
+ `max_limit: ${String(limits.maxInteractiveLimit ?? "")}`,
6592
+ ]);
6593
+ return 0;
6594
+ }
6595
+ async function runDbTables(command, io, runtime) {
6596
+ const credentials = await resolveDbCredentials(command, runtime);
6597
+ const payload = await requestDbApi({
6598
+ artifactId: command.artifactId,
6599
+ dbPath: "tables",
6600
+ method: "GET",
6601
+ baseUrl: credentials.baseUrl,
6602
+ token: credentials.token,
6603
+ fetcher: runtime.fetcher ?? fetch,
6604
+ environment: command.environment,
6605
+ });
6606
+ const rows = Array.isArray(payload.rows) ? payload.rows : [];
6607
+ printDbPayload(command, io, payload, [
6608
+ `app_id: ${command.artifactId}`,
6609
+ `environment: ${command.environment}`,
6610
+ `tables: ${rows.length}`,
6611
+ ]);
6612
+ return 0;
6613
+ }
6614
+ async function runDbSchema(command, io, runtime) {
6615
+ const credentials = await resolveDbCredentials(command, runtime);
6616
+ const payload = await requestDbApi({
6617
+ artifactId: command.artifactId,
6618
+ dbPath: "schema",
6619
+ method: "GET",
6620
+ baseUrl: credentials.baseUrl,
6621
+ token: credentials.token,
6622
+ fetcher: runtime.fetcher ?? fetch,
6623
+ environment: command.environment,
6624
+ tableName: command.tableName,
6625
+ });
6626
+ const rows = Array.isArray(payload.rows) ? payload.rows : [];
6627
+ printDbPayload(command, io, payload, [
6628
+ `app_id: ${command.artifactId}`,
6629
+ `environment: ${command.environment}`,
6630
+ `table: ${command.tableName}`,
6631
+ `schema_rows: ${rows.length}`,
6632
+ ]);
6633
+ return 0;
6634
+ }
6635
+ async function runDbQuery(command, io, runtime) {
6636
+ const credentials = await resolveDbCredentials(command, runtime);
6637
+ const sql = await readDbSql(command, runtime);
6638
+ const body = {
6639
+ environment: command.environment,
6640
+ mode: "read",
6641
+ sql,
6642
+ params: command.params,
6643
+ ...(command.limit === null ? {} : { limit: command.limit }),
6644
+ };
6645
+ const payload = await requestDbApi({
6646
+ artifactId: command.artifactId,
6647
+ dbPath: "query",
6648
+ method: "POST",
6649
+ baseUrl: credentials.baseUrl,
6650
+ token: credentials.token,
6651
+ fetcher: runtime.fetcher ?? fetch,
6652
+ body,
6653
+ });
6654
+ const rows = Array.isArray(payload.rows) ? payload.rows : [];
6655
+ printDbPayload(command, io, payload, [
6656
+ `app_id: ${command.artifactId}`,
6657
+ `environment: ${command.environment}`,
6658
+ `rows: ${rows.length}`,
6659
+ `truncated: ${String(payload.truncated ?? false)}`,
6660
+ ]);
6661
+ return 0;
6662
+ }
6130
6663
  async function runAnalyticsSummary(command, io, runtime) {
6131
6664
  const credentials = await resolveAnalyticsCredentials(command, runtime);
6132
6665
  const payload = await requestAnalyticsApi({
@@ -6660,6 +7193,21 @@ export async function runTenderCli(args, io = {
6660
7193
  else if (command.topic === "app analytics properties activate") {
6661
7194
  io.stdout(analyticsPropertiesActivateHelp());
6662
7195
  }
7196
+ else if (command.topic === "app db") {
7197
+ io.stdout(dbHelp());
7198
+ }
7199
+ else if (command.topic === "app db capabilities") {
7200
+ io.stdout(dbCapabilitiesHelp());
7201
+ }
7202
+ else if (command.topic === "app db tables") {
7203
+ io.stdout(dbTablesHelp());
7204
+ }
7205
+ else if (command.topic === "app db schema") {
7206
+ io.stdout(dbSchemaHelp());
7207
+ }
7208
+ else if (command.topic === "app db query") {
7209
+ io.stdout(dbQueryHelp());
7210
+ }
6663
7211
  else if (command.topic === "app") {
6664
7212
  io.stdout(appHelp());
6665
7213
  }
@@ -6776,6 +7324,18 @@ export async function runTenderCli(args, io = {
6776
7324
  if (command.command === "app analytics properties activate") {
6777
7325
  return await runAnalyticsPropertiesActivate(command, io, runtime);
6778
7326
  }
7327
+ if (command.command === "app db capabilities") {
7328
+ return await runDbCapabilities(command, io, runtime);
7329
+ }
7330
+ if (command.command === "app db tables") {
7331
+ return await runDbTables(command, io, runtime);
7332
+ }
7333
+ if (command.command === "app db schema") {
7334
+ return await runDbSchema(command, io, runtime);
7335
+ }
7336
+ if (command.command === "app db query") {
7337
+ return await runDbQuery(command, io, runtime);
7338
+ }
6779
7339
  if (command.command === "app git remote") {
6780
7340
  return await runGitRemote(command, io, runtime);
6781
7341
  }
@@ -6788,10 +7348,16 @@ export async function runTenderCli(args, io = {
6788
7348
  }
6789
7349
  }
6790
7350
  const currentFile = fileURLToPath(import.meta.url);
7351
+ function hasFlagValue(args, flag, value) {
7352
+ return args.some((arg, index) => arg === flag && args[index + 1] === value);
7353
+ }
6791
7354
  function shouldReadEntrypointStdin(args) {
6792
7355
  return (args[0] === "app" &&
6793
7356
  ((args[1] === "git" && args[2] === "credential") ||
6794
- (args[1] === "analytics" && args.includes("--spec") && args.includes("-"))));
7357
+ (args[1] === "analytics" && hasFlagValue(args, "--spec", "-")) ||
7358
+ (args[1] === "db" &&
7359
+ args[2] === "query" &&
7360
+ hasFlagValue(args, "--file", "-"))));
6795
7361
  }
6796
7362
  async function readEntrypointStdin() {
6797
7363
  if (process.stdin.isTTY) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenderprompt/cli",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "tender": "bin/tender.js"