@tenderprompt/cli 0.1.25 → 0.1.26

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 +27 -0
  2. package/dist/index.js +760 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -39,6 +39,8 @@ session:
39
39
  - inspect artifact-scoped analytics without SQL or platform credentials
40
40
  - inspect artifact-scoped app database tables, schema, and bounded read-only
41
41
  query output
42
+ - create private app admin dashboard, resource, view, and widget definitions
43
+ from structured app database specs
42
44
  - share app setup between Tender accounts without copying customer data,
43
45
  analytics history, app database rows, secrets, or release history
44
46
  - run server-validated chart specs and export aggregate rows
@@ -156,6 +158,11 @@ tender app db tables artifact_123 --env published --json
156
158
  tender app db schema artifact_123 --env published --table votes --json
157
159
  tender app db query artifact_123 --env published --sql "select * from votes limit 20" --json
158
160
  tender app db query artifact_123 --env published --sql "select * from votes order by created_at" --limit 50 --offset 50 --json
161
+ tender app db dashboards artifact_123 --create "App admin" --default-env published --dry-run --json
162
+ tender app db resources suggest artifact_123 --env published --json
163
+ tender app db dashboards render artifact_123 --dashboard app_db_dashboard_123 --env published --json
164
+ tender app db resources create artifact_123 --dashboard app_db_dashboard_123 --spec resource.json --dry-run --json
165
+ tender app db widgets create artifact_123 --dashboard app_db_dashboard_123 --title "Total signups" --spec widget.json --dry-run --json
159
166
  ```
160
167
 
161
168
  ## Shared app setup
@@ -340,6 +347,26 @@ Client and server limits include a 100 KB statement cap and at most 100 bound
340
347
  parameters. The server enforces the final policy, row limits, response limits,
341
348
  artifact scope, and audit logging.
342
349
 
350
+ Private app admin dashboards are definition-only in the CLI. Use the same
351
+ artifact-scoped `db-read` profile to save dashboards, table-backed resources,
352
+ saved views, and widgets; raw app rows stay in the generated app database.
353
+ Logged-in users can open saved dashboards at `/#/workspace/<artifact-id>/admin`.
354
+
355
+ ```bash
356
+ tender app db resources suggest artifact_123 --profile db --env published --json
357
+ tender app db dashboards artifact_123 --profile db --create "Coffee Matcher Admin" --default-env published --json
358
+ tender app db resources create artifact_123 --profile db --dashboard app_db_dashboard_123 --spec resource.json --view recent.json --json
359
+ tender app db widgets create artifact_123 --profile db --dashboard app_db_dashboard_123 --title "Votes by choice" --spec widget.json --json
360
+ tender app db dashboards render artifact_123 --profile db --dashboard app_db_dashboard_123 --env published --json
361
+ ```
362
+
363
+ Suggested app database resources may include `dateRange: { "source":
364
+ "dashboard", "column": "created_at" }` so saved admin dashboards behave like a
365
+ Grafana dashboard: one top-level date range applies to charts and tables by
366
+ default, while resource filters stay available for local drill-downs. Use
367
+ `dateRange: { "source": "off" }` or a fixed override only for panels that should
368
+ not follow the dashboard date range.
369
+
343
370
  ## Safety Model
344
371
 
345
372
  - Local agents run in the user's environment, not in Tender.
package/dist/index.js CHANGED
@@ -189,6 +189,8 @@ Commands:
189
189
  Describe app database query limits and policy
190
190
  tender app db query <app-id> --env <preview|published> --sql <sql>
191
191
  Run one read-only app database query
192
+ tender app db dashboards <app-id>
193
+ List or create private app admin dashboards
192
194
  tender app context fetch <app-id>
193
195
  Fetch managed agent context into a checkout
194
196
  tender app context status <app-id>
@@ -451,6 +453,12 @@ Commands:
451
453
  Show table and index schema for one table
452
454
  tender app db query <app-id> --env <preview|published> --sql <sql>
453
455
  Run one read-only app database query
456
+ tender app db dashboards <app-id>
457
+ List or create private app admin dashboards
458
+ tender app db resources create <app-id>
459
+ Add a table-backed dashboard resource
460
+ tender app db widgets create <app-id>
461
+ Add a saved dashboard widget
454
462
  tender app context fetch <app-id>
455
463
  Fetch AGENTS.md, skills, and Tender project context
456
464
  tender app context status <app-id>
@@ -792,12 +800,27 @@ Commands:
792
800
  Show schema for one table
793
801
  tender app db query <app-id> --env <preview|published>
794
802
  Run one read-only SQL query
803
+ tender app db dashboards <app-id>
804
+ List or create private app admin dashboards
805
+ tender app db resources suggest <app-id>
806
+ Suggest private app admin resources from schema
807
+ tender app db dashboards render <app-id>
808
+ Render saved private app admin queries
809
+ tender app db resources create <app-id>
810
+ Add a table-backed dashboard resource from JSON
811
+ tender app db widgets create <app-id>
812
+ Add a saved dashboard widget from JSON
795
813
 
796
814
  Examples:
797
815
  tender app db capabilities artifact_123 --json
798
816
  tender app db tables artifact_123 --env published --json
799
817
  tender app db schema artifact_123 --env published --table votes --json
800
818
  tender app db query artifact_123 --env published --sql "select * from votes limit 20" --json
819
+ tender app db resources suggest artifact_123 --env published --json
820
+ tender app db dashboards artifact_123 --create "Coffee Matcher Admin" --json
821
+ tender app db dashboards render artifact_123 --dashboard app_db_dashboard_123 --env published --json
822
+ tender app db resources create artifact_123 --dashboard app_db_dashboard_123 --spec resource.json --json
823
+ tender app db widgets create artifact_123 --dashboard app_db_dashboard_123 --title "Total signups" --spec widget.json --json
801
824
  tender app db query artifact_123 --env published --file ./debug.sql --param active --limit 50 --json`;
802
825
  }
803
826
  function dbCapabilitiesHelp() {
@@ -837,6 +860,38 @@ Examples:
837
860
  tender app db query artifact_123 --env published --file ./debug.sql --limit 50 --offset 100 --json
838
861
  tender app db query artifact_123 --env published --file - --json < debug.sql`;
839
862
  }
863
+ function dbDashboardsHelp() {
864
+ return `Usage: tender app db dashboards <app-id> [--create <name>] [--default-env <preview|published>] [--dry-run] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
865
+
866
+ Examples:
867
+ tender app db dashboards artifact_123 --json
868
+ tender app db dashboards artifact_123 --create "Coffee Matcher Admin" --default-env published --json`;
869
+ }
870
+ function dbDashboardsRenderHelp() {
871
+ return `Usage: tender app db dashboards render <app-id> --dashboard <dashboard-id> --env <preview|published> [--base-url <url>] [--token <token>] [--profile <name>] [--json]
872
+
873
+ Examples:
874
+ tender app db dashboards render artifact_123 --dashboard app_db_dashboard_123 --env published --json`;
875
+ }
876
+ function dbResourcesSuggestHelp() {
877
+ return `Usage: tender app db resources suggest <app-id> --env <preview|published> [--base-url <url>] [--token <token>] [--profile <name>] [--json]
878
+
879
+ Examples:
880
+ tender app db resources suggest artifact_123 --env published --json`;
881
+ }
882
+ function dbResourcesCreateHelp() {
883
+ return `Usage: tender app db resources create <app-id> --dashboard <dashboard-id> --spec <resource.json|-> [--view <view.json|->] [--dry-run] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
884
+
885
+ Examples:
886
+ tender app db resources create artifact_123 --dashboard app_db_dashboard_123 --spec resource.json --json
887
+ tender app db resources create artifact_123 --dashboard app_db_dashboard_123 --spec resource.json --view needs-review.json --json`;
888
+ }
889
+ function dbWidgetsCreateHelp() {
890
+ return `Usage: tender app db widgets create <app-id> --dashboard <dashboard-id> --title <title> --spec <widget.json|-> [--dry-run] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
891
+
892
+ Examples:
893
+ tender app db widgets create artifact_123 --dashboard app_db_dashboard_123 --title "Total signups" --spec widget.json --json`;
894
+ }
840
895
  function artifactsListHelp() {
841
896
  return `Usage: tender app list [--base-url <url>] [--token <token>] [--profile <name>] [--json]
842
897
 
@@ -1408,11 +1463,14 @@ function tenderCliCapabilities() {
1408
1463
  'tender app db schema <artifact-id> --env published --table <table-name> --profile db --json',
1409
1464
  'tender app db query <artifact-id> --env published --sql "select * from <table-name> limit 20" --profile db --json',
1410
1465
  'tender app db query <artifact-id> --env published --file ./debug.sql --profile db --json',
1466
+ 'tender app db resources suggest <artifact-id> --env published --profile db --json',
1467
+ 'tender app db dashboards render <artifact-id> --dashboard <dashboard-id> --env published --profile db --json',
1411
1468
  ],
1412
1469
  notes: [
1413
1470
  'The CLI only accepts artifact ids. It never accepts tenant ids, runner names, provider account ids, or runtime storage identifiers.',
1414
1471
  'Read-only policy is enforced by the server before the runtime shim executes SQL.',
1415
1472
  'Use --file - to pipe SQL from stdin without prompts.',
1473
+ 'Use resources suggest and dashboards render for private operator/admin surfaces instead of building database tables into shopper UI.',
1416
1474
  ],
1417
1475
  },
1418
1476
  {
@@ -1611,6 +1669,39 @@ function tenderCliCapabilities() {
1611
1669
  requiresAuth: true,
1612
1670
  writes: false,
1613
1671
  },
1672
+ {
1673
+ command: 'tender app db resources suggest <artifact-id> --env published --json',
1674
+ purpose: 'Suggest private app admin resources, saved views, and widgets from app database schema and samples.',
1675
+ requiresAuth: true,
1676
+ writes: false,
1677
+ },
1678
+ {
1679
+ command: 'tender app db dashboards <artifact-id> --create "App admin" --dry-run --json',
1680
+ purpose: 'Preview or create a private app database dashboard definition for one artifact.',
1681
+ requiresAuth: true,
1682
+ writes: true,
1683
+ dryRun: true,
1684
+ },
1685
+ {
1686
+ command: 'tender app db dashboards render <artifact-id> --dashboard <dashboard-id> --env published --json',
1687
+ purpose: 'Render saved private app admin resource and widget queries for completion proof.',
1688
+ requiresAuth: true,
1689
+ writes: false,
1690
+ },
1691
+ {
1692
+ command: 'tender app db resources create <artifact-id> --dashboard <dashboard-id> --spec resource.json --dry-run --json',
1693
+ purpose: 'Preview or save a table-backed resource definition for a private app admin dashboard.',
1694
+ requiresAuth: true,
1695
+ writes: true,
1696
+ dryRun: true,
1697
+ },
1698
+ {
1699
+ command: 'tender app db widgets create <artifact-id> --dashboard <dashboard-id> --title <title> --spec widget.json --dry-run --json',
1700
+ purpose: 'Preview or save a bounded widget definition for a private app admin dashboard.',
1701
+ requiresAuth: true,
1702
+ writes: true,
1703
+ dryRun: true,
1704
+ },
1614
1705
  ],
1615
1706
  security: [
1616
1707
  'Prefer artifact-scoped device tokens.',
@@ -5414,6 +5505,239 @@ function parseDbQueryArgs(args) {
5414
5505
  ...parsed,
5415
5506
  };
5416
5507
  }
5508
+ function parseDbDashboardsArgs(args) {
5509
+ if (args.includes('--help') || args.includes('-h')) {
5510
+ return { command: 'help', topic: 'app db dashboards' };
5511
+ }
5512
+ const artifactId = args[0];
5513
+ if (!artifactId || artifactId.startsWith('-')) {
5514
+ throw new TenderCliUsageError('app id is required. Example: tender app db dashboards <app-id> --json');
5515
+ }
5516
+ const parsed = parseDbCommonArgs(args, 1);
5517
+ let name = null;
5518
+ let defaultEnvironment = null;
5519
+ let dryRun = false;
5520
+ for (let index = 0; index < parsed.remaining.length; index += 1) {
5521
+ const arg = parsed.remaining[index];
5522
+ const defaultEnvAssignment = parseFlagAssignment(arg, '--default-env');
5523
+ if (defaultEnvAssignment !== null) {
5524
+ defaultEnvironment = parseDbEnvironment(defaultEnvAssignment);
5525
+ continue;
5526
+ }
5527
+ if (arg === '--create') {
5528
+ name = parseRequiredFlagValue(parsed.remaining[index + 1], '--create');
5529
+ index += 1;
5530
+ continue;
5531
+ }
5532
+ if (arg === '--default-env') {
5533
+ defaultEnvironment = parseDbEnvironment(parsed.remaining[index + 1]);
5534
+ index += 1;
5535
+ continue;
5536
+ }
5537
+ if (arg === '--dry-run') {
5538
+ dryRun = true;
5539
+ continue;
5540
+ }
5541
+ throw new TenderCliUsageError(`Unknown app db dashboards option: ${arg}`);
5542
+ }
5543
+ if (dryRun && !name) {
5544
+ throw new TenderCliUsageError('--dry-run is only valid with --create. Example: tender app db dashboards artifact_123 --create "App admin" --dry-run --json');
5545
+ }
5546
+ return { command: 'app db dashboards', artifactId, name, defaultEnvironment, dryRun, ...parsed };
5547
+ }
5548
+ function parseDbDashboardsRenderArgs(args) {
5549
+ if (args.includes('--help') || args.includes('-h')) {
5550
+ return { command: 'help', topic: 'app db dashboards render' };
5551
+ }
5552
+ const artifactId = args[0];
5553
+ if (!artifactId || artifactId.startsWith('-')) {
5554
+ throw new TenderCliUsageError('app id is required. Example: tender app db dashboards render <app-id> --dashboard app_db_dashboard_123 --env published --json');
5555
+ }
5556
+ const parsed = parseDbCommonArgs(args, 1);
5557
+ let dashboardId = null;
5558
+ let environment = null;
5559
+ for (let index = 0; index < parsed.remaining.length; index += 1) {
5560
+ const arg = parsed.remaining[index];
5561
+ const dashboardAssignment = parseFlagAssignment(arg, '--dashboard');
5562
+ if (dashboardAssignment !== null) {
5563
+ dashboardId = parseRequiredFlagValue(dashboardAssignment, '--dashboard');
5564
+ continue;
5565
+ }
5566
+ const envAssignment = parseFlagAssignment(arg, '--env');
5567
+ if (envAssignment !== null) {
5568
+ environment = parseDbEnvironment(envAssignment);
5569
+ continue;
5570
+ }
5571
+ if (arg === '--dashboard') {
5572
+ dashboardId = parseRequiredFlagValue(parsed.remaining[index + 1], '--dashboard');
5573
+ index += 1;
5574
+ continue;
5575
+ }
5576
+ if (arg === '--env') {
5577
+ environment = parseDbEnvironment(parsed.remaining[index + 1]);
5578
+ index += 1;
5579
+ continue;
5580
+ }
5581
+ throw new TenderCliUsageError(`Unknown app db dashboards render option: ${arg}`);
5582
+ }
5583
+ if (!dashboardId) {
5584
+ throw new TenderCliUsageError('--dashboard is required. Example: tender app db dashboards render artifact_123 --dashboard app_db_dashboard_123 --env published --json');
5585
+ }
5586
+ if (!environment) {
5587
+ throw new TenderCliUsageError('--env is required. Example: tender app db dashboards render artifact_123 --dashboard app_db_dashboard_123 --env published --json');
5588
+ }
5589
+ return { command: 'app db dashboards render', artifactId, dashboardId, environment, ...parsed };
5590
+ }
5591
+ function parseDbResourcesSuggestArgs(args) {
5592
+ if (args.includes('--help') || args.includes('-h')) {
5593
+ return { command: 'help', topic: 'app db resources suggest' };
5594
+ }
5595
+ const artifactId = args[0];
5596
+ if (!artifactId || artifactId.startsWith('-')) {
5597
+ throw new TenderCliUsageError('app id is required. Example: tender app db resources suggest <app-id> --env published --json');
5598
+ }
5599
+ const parsed = parseDbCommonArgs(args, 1);
5600
+ let environment = null;
5601
+ for (let index = 0; index < parsed.remaining.length; index += 1) {
5602
+ const arg = parsed.remaining[index];
5603
+ const envAssignment = parseFlagAssignment(arg, '--env');
5604
+ if (envAssignment !== null) {
5605
+ environment = parseDbEnvironment(envAssignment);
5606
+ continue;
5607
+ }
5608
+ if (arg === '--env') {
5609
+ environment = parseDbEnvironment(parsed.remaining[index + 1]);
5610
+ index += 1;
5611
+ continue;
5612
+ }
5613
+ throw new TenderCliUsageError(`Unknown app db resources suggest option: ${arg}`);
5614
+ }
5615
+ if (!environment) {
5616
+ throw new TenderCliUsageError('--env is required. Example: tender app db resources suggest artifact_123 --env published --json');
5617
+ }
5618
+ return { command: 'app db resources suggest', artifactId, environment, ...parsed };
5619
+ }
5620
+ function parseDbResourcesCreateArgs(args) {
5621
+ if (args.includes('--help') || args.includes('-h')) {
5622
+ return { command: 'help', topic: 'app db resources create' };
5623
+ }
5624
+ const artifactId = args[0];
5625
+ if (!artifactId || artifactId.startsWith('-')) {
5626
+ throw new TenderCliUsageError('app id is required. Example: tender app db resources create <app-id> --dashboard app_db_dashboard_123 --spec resource.json --json');
5627
+ }
5628
+ const parsed = parseDbCommonArgs(args, 1);
5629
+ let dashboardId = null;
5630
+ let specPath = null;
5631
+ const viewPaths = [];
5632
+ let dryRun = false;
5633
+ for (let index = 0; index < parsed.remaining.length; index += 1) {
5634
+ const arg = parsed.remaining[index];
5635
+ const dashboardAssignment = parseFlagAssignment(arg, '--dashboard');
5636
+ if (dashboardAssignment !== null) {
5637
+ dashboardId = parseRequiredFlagValue(dashboardAssignment, '--dashboard');
5638
+ continue;
5639
+ }
5640
+ const specAssignment = parseFlagAssignment(arg, '--spec');
5641
+ if (specAssignment !== null) {
5642
+ specPath = parseSpecPath(specAssignment);
5643
+ continue;
5644
+ }
5645
+ const viewAssignment = parseFlagAssignment(arg, '--view');
5646
+ if (viewAssignment !== null) {
5647
+ viewPaths.push(parseSpecPath(viewAssignment));
5648
+ continue;
5649
+ }
5650
+ if (arg === '--dashboard') {
5651
+ dashboardId = parseRequiredFlagValue(parsed.remaining[index + 1], '--dashboard');
5652
+ index += 1;
5653
+ continue;
5654
+ }
5655
+ if (arg === '--spec') {
5656
+ specPath = parseSpecPath(parsed.remaining[index + 1]);
5657
+ index += 1;
5658
+ continue;
5659
+ }
5660
+ if (arg === '--view') {
5661
+ viewPaths.push(parseSpecPath(parsed.remaining[index + 1]));
5662
+ index += 1;
5663
+ continue;
5664
+ }
5665
+ if (arg === '--dry-run') {
5666
+ dryRun = true;
5667
+ continue;
5668
+ }
5669
+ throw new TenderCliUsageError(`Unknown app db resources create option: ${arg}`);
5670
+ }
5671
+ if (!dashboardId) {
5672
+ throw new TenderCliUsageError('--dashboard is required. Example: tender app db resources create artifact_123 --dashboard app_db_dashboard_123 --spec resource.json --json');
5673
+ }
5674
+ if (!specPath) {
5675
+ throw new TenderCliUsageError('--spec is required. Example: tender app db resources create artifact_123 --dashboard app_db_dashboard_123 --spec resource.json --json');
5676
+ }
5677
+ return { command: 'app db resources create', artifactId, dashboardId, specPath, viewPaths, dryRun, ...parsed };
5678
+ }
5679
+ function parseDbWidgetsCreateArgs(args) {
5680
+ if (args.includes('--help') || args.includes('-h')) {
5681
+ return { command: 'help', topic: 'app db widgets create' };
5682
+ }
5683
+ const artifactId = args[0];
5684
+ if (!artifactId || artifactId.startsWith('-')) {
5685
+ throw new TenderCliUsageError('app id is required. Example: tender app db widgets create <app-id> --dashboard app_db_dashboard_123 --title "Total signups" --spec widget.json --json');
5686
+ }
5687
+ const parsed = parseDbCommonArgs(args, 1);
5688
+ let dashboardId = null;
5689
+ let title = null;
5690
+ let specPath = null;
5691
+ let dryRun = false;
5692
+ for (let index = 0; index < parsed.remaining.length; index += 1) {
5693
+ const arg = parsed.remaining[index];
5694
+ const dashboardAssignment = parseFlagAssignment(arg, '--dashboard');
5695
+ if (dashboardAssignment !== null) {
5696
+ dashboardId = parseRequiredFlagValue(dashboardAssignment, '--dashboard');
5697
+ continue;
5698
+ }
5699
+ const titleAssignment = parseFlagAssignment(arg, '--title');
5700
+ if (titleAssignment !== null) {
5701
+ title = parseRequiredFlagValue(titleAssignment, '--title');
5702
+ continue;
5703
+ }
5704
+ const specAssignment = parseFlagAssignment(arg, '--spec');
5705
+ if (specAssignment !== null) {
5706
+ specPath = parseSpecPath(specAssignment);
5707
+ continue;
5708
+ }
5709
+ if (arg === '--dashboard') {
5710
+ dashboardId = parseRequiredFlagValue(parsed.remaining[index + 1], '--dashboard');
5711
+ index += 1;
5712
+ continue;
5713
+ }
5714
+ if (arg === '--title') {
5715
+ title = parseRequiredFlagValue(parsed.remaining[index + 1], '--title');
5716
+ index += 1;
5717
+ continue;
5718
+ }
5719
+ if (arg === '--spec') {
5720
+ specPath = parseSpecPath(parsed.remaining[index + 1]);
5721
+ index += 1;
5722
+ continue;
5723
+ }
5724
+ if (arg === '--dry-run') {
5725
+ dryRun = true;
5726
+ continue;
5727
+ }
5728
+ throw new TenderCliUsageError(`Unknown app db widgets create option: ${arg}`);
5729
+ }
5730
+ if (!dashboardId) {
5731
+ throw new TenderCliUsageError('--dashboard is required. Example: tender app db widgets create artifact_123 --dashboard app_db_dashboard_123 --title "Total signups" --spec widget.json --json');
5732
+ }
5733
+ if (!title) {
5734
+ throw new TenderCliUsageError('--title is required. Example: tender app db widgets create artifact_123 --dashboard app_db_dashboard_123 --title "Total signups" --spec widget.json --json');
5735
+ }
5736
+ if (!specPath) {
5737
+ throw new TenderCliUsageError('--spec is required. Example: tender app db widgets create artifact_123 --dashboard app_db_dashboard_123 --title "Total signups" --spec widget.json --json');
5738
+ }
5739
+ return { command: 'app db widgets create', artifactId, dashboardId, title, specPath, dryRun, ...parsed };
5740
+ }
5417
5741
  const AGENT_HEARTBEAT_SOURCE_VALUES = new Set(['codex', 'claude-code', 'tender-cli', 'git', 'other']);
5418
5742
  const AGENT_HEARTBEAT_STATUS_VALUES = new Set(['connected', 'working', 'waiting_for_user', 'needs_attention']);
5419
5743
  const AGENT_HEARTBEAT_PHASE_VALUES = new Set([
@@ -5859,6 +6183,31 @@ function parseTenderArgs(args) {
5859
6183
  if (resourceArgs[1] === 'db' && resourceArgs[2] === 'query') {
5860
6184
  return parseDbQueryArgs(resourceArgs.slice(3));
5861
6185
  }
6186
+ if (resourceArgs[1] === 'db' && resourceArgs[2] === 'dashboards' && resourceArgs[3] === 'render') {
6187
+ return parseDbDashboardsRenderArgs(resourceArgs.slice(4));
6188
+ }
6189
+ if (resourceArgs[1] === 'db' && resourceArgs[2] === 'dashboards') {
6190
+ return parseDbDashboardsArgs(resourceArgs.slice(3));
6191
+ }
6192
+ if (resourceArgs[1] === 'db' &&
6193
+ resourceArgs[2] === 'resources' &&
6194
+ (resourceArgs[3] === undefined || resourceArgs[3] === '--help' || resourceArgs[3] === '-h')) {
6195
+ return { command: 'help', topic: 'app db resources create' };
6196
+ }
6197
+ if (resourceArgs[1] === 'db' && resourceArgs[2] === 'resources' && resourceArgs[3] === 'suggest') {
6198
+ return parseDbResourcesSuggestArgs(resourceArgs.slice(4));
6199
+ }
6200
+ if (resourceArgs[1] === 'db' && resourceArgs[2] === 'resources' && resourceArgs[3] === 'create') {
6201
+ return parseDbResourcesCreateArgs(resourceArgs.slice(4));
6202
+ }
6203
+ if (resourceArgs[1] === 'db' &&
6204
+ resourceArgs[2] === 'widgets' &&
6205
+ (resourceArgs[3] === undefined || resourceArgs[3] === '--help' || resourceArgs[3] === '-h')) {
6206
+ return { command: 'help', topic: 'app db widgets create' };
6207
+ }
6208
+ if (resourceArgs[1] === 'db' && resourceArgs[2] === 'widgets' && resourceArgs[3] === 'create') {
6209
+ return parseDbWidgetsCreateArgs(resourceArgs.slice(4));
6210
+ }
5862
6211
  if (resourceArgs[1] === 'db') {
5863
6212
  throw new TenderCliUsageError(`Unknown app db command: ${resourceArgs[2]}`);
5864
6213
  }
@@ -6300,6 +6649,66 @@ async function requestDbApi(input) {
6300
6649
  }
6301
6650
  return payload;
6302
6651
  }
6652
+ function appDbDashboardApiSuffix(input) {
6653
+ if (input.path === 'dashboards') {
6654
+ return '';
6655
+ }
6656
+ if (input.path === 'suggest') {
6657
+ return '/suggest';
6658
+ }
6659
+ if (!input.dashboardId) {
6660
+ throw new TenderCliUsageError('--dashboard is required.');
6661
+ }
6662
+ if (input.path === 'dashboard') {
6663
+ return `/${encodeURIComponent(input.dashboardId)}`;
6664
+ }
6665
+ if (input.path === 'resources') {
6666
+ return `/${encodeURIComponent(input.dashboardId)}/resources`;
6667
+ }
6668
+ if (input.path === 'resourceQuery') {
6669
+ if (!input.resourceId) {
6670
+ throw new TenderCliUsageError('--resource is required.');
6671
+ }
6672
+ return `/${encodeURIComponent(input.dashboardId)}/resources/${encodeURIComponent(input.resourceId)}/query`;
6673
+ }
6674
+ if (input.path === 'widgets') {
6675
+ return `/${encodeURIComponent(input.dashboardId)}/widgets`;
6676
+ }
6677
+ if (!input.widgetId) {
6678
+ throw new TenderCliUsageError('--widget is required.');
6679
+ }
6680
+ return `/${encodeURIComponent(input.dashboardId)}/widgets/${encodeURIComponent(input.widgetId)}/query`;
6681
+ }
6682
+ async function requestAppDbDashboardApi(input) {
6683
+ const suffix = appDbDashboardApiSuffix(input);
6684
+ const response = await input.fetcher(`${input.baseUrl}/api/v1/artifacts/${encodeURIComponent(input.artifactId)}/app-db-dashboards${suffix}`, {
6685
+ method: input.method,
6686
+ headers: {
6687
+ authorization: `Bearer ${input.token}`,
6688
+ accept: 'application/json',
6689
+ ...(input.body === undefined ? {} : { 'content-type': 'application/json' }),
6690
+ },
6691
+ body: input.body === undefined ? undefined : JSON.stringify(input.body),
6692
+ });
6693
+ const payload = await parseJsonResponse(response);
6694
+ if (!response.ok || !payload?.ok) {
6695
+ const reasonCode = (typeof payload?.reasonCode === 'string' && payload.reasonCode) || `http_${response.status}`;
6696
+ const message = (typeof payload?.message === 'string' && payload.message) ||
6697
+ (typeof payload?.reasonCode === 'string' && payload.reasonCode) ||
6698
+ `App database dashboard request failed with HTTP ${response.status}.`;
6699
+ const example = appDbDashboardCorrectedExample(input.artifactId, input.path);
6700
+ throw new TenderCliDbApiError([
6701
+ message,
6702
+ `Scope: artifact ${input.artifactId}, app-db-dashboards.`,
6703
+ `Reason: ${reasonCode}.`,
6704
+ `Example: ${example}.`,
6705
+ ].join('\n'), reasonCode, {
6706
+ artifactId: input.artifactId,
6707
+ surface: 'db/dashboards',
6708
+ }, example);
6709
+ }
6710
+ return payload;
6711
+ }
6303
6712
  function encodePathSegments(filePath) {
6304
6713
  return filePath.split('/').map(encodeURIComponent).join('/');
6305
6714
  }
@@ -6341,6 +6750,21 @@ function dbCorrectedExample(artifactId, dbPath) {
6341
6750
  }
6342
6751
  return `tender app db query ${artifactId} --env published --sql "select * from <table-name> limit 20" --json`;
6343
6752
  }
6753
+ function appDbDashboardCorrectedExample(artifactId, path) {
6754
+ if (path === 'suggest') {
6755
+ return `tender app db resources suggest ${artifactId} --env published --json`;
6756
+ }
6757
+ if (path === 'dashboard' || path === 'resourceQuery' || path === 'widgetQuery') {
6758
+ return `tender app db dashboards render ${artifactId} --dashboard app_db_dashboard_123 --env published --json`;
6759
+ }
6760
+ if (path === 'resources') {
6761
+ return `tender app db resources create ${artifactId} --dashboard app_db_dashboard_123 --spec resource.json --json`;
6762
+ }
6763
+ if (path === 'widgets') {
6764
+ return `tender app db widgets create ${artifactId} --dashboard app_db_dashboard_123 --title "Total signups" --spec widget.json --json`;
6765
+ }
6766
+ return `tender app db dashboards ${artifactId} --json`;
6767
+ }
6344
6768
  function analyticsCorrectedExample(artifactId, analyticsPath) {
6345
6769
  if (analyticsPath === 'capabilities') {
6346
6770
  return `tender app analytics capabilities ${artifactId} --include-catalog --range 30d --json`;
@@ -9755,6 +10179,310 @@ async function runDbQuery(command, io, runtime) {
9755
10179
  ]);
9756
10180
  return 0;
9757
10181
  }
10182
+ async function runDbDashboards(command, io, runtime) {
10183
+ if (command.name && command.dryRun) {
10184
+ const payload = {
10185
+ ok: true,
10186
+ dryRun: true,
10187
+ artifactId: command.artifactId,
10188
+ request: {
10189
+ name: command.name,
10190
+ defaultEnvironment: command.defaultEnvironment ?? 'published',
10191
+ },
10192
+ };
10193
+ printDbPayload(command, io, payload, [`app_id: ${command.artifactId}`, `name: ${command.name}`, 'dry_run: true']);
10194
+ return 0;
10195
+ }
10196
+ const credentials = await resolveDbCredentials(command, runtime);
10197
+ const payload = await requestAppDbDashboardApi({
10198
+ artifactId: command.artifactId,
10199
+ path: 'dashboards',
10200
+ method: command.name ? 'POST' : 'GET',
10201
+ baseUrl: credentials.baseUrl,
10202
+ token: credentials.token,
10203
+ fetcher: runtime.fetcher ?? fetch,
10204
+ body: command.name
10205
+ ? {
10206
+ name: command.name,
10207
+ defaultEnvironment: command.defaultEnvironment ?? 'published',
10208
+ }
10209
+ : undefined,
10210
+ });
10211
+ const dashboards = Array.isArray(payload.dashboards) ? payload.dashboards : [];
10212
+ printDbPayload(command, io, payload, [
10213
+ `app_id: ${command.artifactId}`,
10214
+ command.name
10215
+ ? `dashboard_id: ${String(payload.dashboard?.id ?? '')}`
10216
+ : `dashboards: ${dashboards.length}`,
10217
+ ]);
10218
+ return 0;
10219
+ }
10220
+ function isAppDbRecord(value) {
10221
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
10222
+ }
10223
+ function appDbRecordArray(value) {
10224
+ return Array.isArray(value) ? value.filter(isAppDbRecord) : [];
10225
+ }
10226
+ function appDbStringField(record, field) {
10227
+ const value = record[field];
10228
+ return typeof value === 'string' && value.trim() ? value : null;
10229
+ }
10230
+ function appDbResourcePageSize(resource) {
10231
+ const spec = isAppDbRecord(resource.spec) ? resource.spec : null;
10232
+ const pageSize = spec && typeof spec.pageSize === 'number' && Number.isInteger(spec.pageSize) ? spec.pageSize : 50;
10233
+ return Math.min(Math.max(pageSize, 1), 50);
10234
+ }
10235
+ function appDbDateRangeDays(value) {
10236
+ if (value === 'last_7_days') {
10237
+ return 7;
10238
+ }
10239
+ if (value === 'last_90_days') {
10240
+ return 90;
10241
+ }
10242
+ return 30;
10243
+ }
10244
+ function appDbDashboardDateRangeRequest(dashboard, now) {
10245
+ const spec = isAppDbRecord(dashboard.spec) ? dashboard.spec : {};
10246
+ const controls = isAppDbRecord(spec.controls) ? spec.controls : {};
10247
+ const dateRange = isAppDbRecord(controls.dateRange) ? controls.dateRange : {};
10248
+ const enabled = dateRange.enabled === undefined ? true : dateRange.enabled === true;
10249
+ const defaultRange = dateRange.defaultRange ?? 'last_30_days';
10250
+ if (enabled &&
10251
+ isAppDbRecord(defaultRange) &&
10252
+ defaultRange.type === 'fixed' &&
10253
+ (typeof defaultRange.start === 'string' || typeof defaultRange.start === 'number') &&
10254
+ (typeof defaultRange.end === 'string' || typeof defaultRange.end === 'number')) {
10255
+ return { start: defaultRange.start, end: defaultRange.end };
10256
+ }
10257
+ const preset = typeof defaultRange === 'string' ? defaultRange : 'last_30_days';
10258
+ if (!enabled || preset === 'all_time') {
10259
+ return null;
10260
+ }
10261
+ const end = new Date(now);
10262
+ const start = new Date(end);
10263
+ start.setDate(start.getDate() - (appDbDateRangeDays(preset) - 1));
10264
+ return {
10265
+ start: start.toISOString().slice(0, 10),
10266
+ end: end.toISOString().slice(0, 10),
10267
+ };
10268
+ }
10269
+ async function runDbResourcesSuggest(command, io, runtime) {
10270
+ const credentials = await resolveDbCredentials(command, runtime);
10271
+ const payload = await requestAppDbDashboardApi({
10272
+ artifactId: command.artifactId,
10273
+ path: 'suggest',
10274
+ method: 'POST',
10275
+ baseUrl: credentials.baseUrl,
10276
+ token: credentials.token,
10277
+ fetcher: runtime.fetcher ?? fetch,
10278
+ body: { environment: command.environment },
10279
+ });
10280
+ const resources = Array.isArray(payload.resources) ? payload.resources : [];
10281
+ printDbPayload(command, io, payload, [
10282
+ `app_id: ${command.artifactId}`,
10283
+ `environment: ${command.environment}`,
10284
+ `suggested_resources: ${resources.length}`,
10285
+ ]);
10286
+ return 0;
10287
+ }
10288
+ async function runDbDashboardsRender(command, io, runtime) {
10289
+ const credentials = await resolveDbCredentials(command, runtime);
10290
+ const dashboardPayload = await requestAppDbDashboardApi({
10291
+ artifactId: command.artifactId,
10292
+ path: 'dashboard',
10293
+ dashboardId: command.dashboardId,
10294
+ method: 'GET',
10295
+ baseUrl: credentials.baseUrl,
10296
+ token: credentials.token,
10297
+ fetcher: runtime.fetcher ?? fetch,
10298
+ });
10299
+ const dashboard = isAppDbRecord(dashboardPayload.dashboard) ? dashboardPayload.dashboard : {};
10300
+ const dateRange = appDbDashboardDateRangeRequest(dashboard, runtime.now ? runtime.now() : Date.now());
10301
+ const resources = appDbRecordArray(dashboard.resources).slice(0, 6);
10302
+ const widgets = appDbRecordArray(dashboard.widgets).slice(0, 12);
10303
+ const resourceResults = [];
10304
+ for (const resource of resources) {
10305
+ const resourceId = appDbStringField(resource, 'id');
10306
+ if (!resourceId) {
10307
+ continue;
10308
+ }
10309
+ const views = [
10310
+ { viewId: null, name: 'Default' },
10311
+ ...appDbRecordArray(resource.views)
10312
+ .slice(0, 4)
10313
+ .map((view) => ({
10314
+ viewId: appDbStringField(view, 'id'),
10315
+ name: appDbStringField(view, 'name') ?? 'Saved view',
10316
+ }))
10317
+ .filter((view) => view.viewId !== null),
10318
+ ];
10319
+ const viewResults = [];
10320
+ for (const view of views) {
10321
+ const result = await requestAppDbDashboardApi({
10322
+ artifactId: command.artifactId,
10323
+ path: 'resourceQuery',
10324
+ dashboardId: command.dashboardId,
10325
+ resourceId,
10326
+ method: 'POST',
10327
+ baseUrl: credentials.baseUrl,
10328
+ token: credentials.token,
10329
+ fetcher: runtime.fetcher ?? fetch,
10330
+ body: {
10331
+ environment: command.environment,
10332
+ ...(view.viewId ? { viewId: view.viewId } : {}),
10333
+ ...(dateRange ? { dateRange } : {}),
10334
+ limit: appDbResourcePageSize(resource),
10335
+ offset: 0,
10336
+ },
10337
+ });
10338
+ viewResults.push({
10339
+ viewId: view.viewId,
10340
+ name: view.name,
10341
+ result,
10342
+ });
10343
+ }
10344
+ resourceResults.push({
10345
+ resourceId,
10346
+ label: appDbStringField(resource, 'label') ?? resourceId,
10347
+ views: viewResults,
10348
+ });
10349
+ }
10350
+ const widgetResults = [];
10351
+ for (const widget of widgets) {
10352
+ const widgetId = appDbStringField(widget, 'id');
10353
+ if (!widgetId) {
10354
+ continue;
10355
+ }
10356
+ const spec = isAppDbRecord(widget.spec) ? widget.spec : null;
10357
+ const specLimit = spec && typeof spec.limit === 'number' && Number.isInteger(spec.limit) ? spec.limit : 20;
10358
+ const result = await requestAppDbDashboardApi({
10359
+ artifactId: command.artifactId,
10360
+ path: 'widgetQuery',
10361
+ dashboardId: command.dashboardId,
10362
+ widgetId,
10363
+ method: 'POST',
10364
+ baseUrl: credentials.baseUrl,
10365
+ token: credentials.token,
10366
+ fetcher: runtime.fetcher ?? fetch,
10367
+ body: {
10368
+ environment: command.environment,
10369
+ ...(dateRange ? { dateRange } : {}),
10370
+ limit: Math.min(Math.max(specLimit, 1), 50),
10371
+ offset: 0,
10372
+ },
10373
+ });
10374
+ widgetResults.push({
10375
+ widgetId,
10376
+ title: appDbStringField(widget, 'title') ?? widgetId,
10377
+ result,
10378
+ });
10379
+ }
10380
+ const payload = {
10381
+ ok: true,
10382
+ artifactId: command.artifactId,
10383
+ environment: command.environment,
10384
+ dashboard,
10385
+ resources: resourceResults,
10386
+ widgets: widgetResults,
10387
+ };
10388
+ printDbPayload(command, io, payload, [
10389
+ `app_id: ${command.artifactId}`,
10390
+ `dashboard_id: ${command.dashboardId}`,
10391
+ `environment: ${command.environment}`,
10392
+ `resources_rendered: ${resourceResults.length}`,
10393
+ `widgets_rendered: ${widgetResults.length}`,
10394
+ ]);
10395
+ return 0;
10396
+ }
10397
+ async function readJsonSpecFile(input) {
10398
+ return readAnalyticsSpec({
10399
+ specPath: input.specPath,
10400
+ stdin: input.stdin,
10401
+ });
10402
+ }
10403
+ async function runDbResourcesCreate(command, io, runtime) {
10404
+ const spec = await readJsonSpecFile({
10405
+ specPath: command.specPath,
10406
+ stdin: runtime.stdin,
10407
+ });
10408
+ const views = [];
10409
+ for (const viewPath of command.viewPaths) {
10410
+ views.push({
10411
+ spec: await readJsonSpecFile({
10412
+ specPath: viewPath,
10413
+ stdin: runtime.stdin,
10414
+ }),
10415
+ });
10416
+ }
10417
+ const body = {
10418
+ spec,
10419
+ ...(views.length > 0 ? { views } : {}),
10420
+ };
10421
+ if (command.dryRun) {
10422
+ printDbPayload(command, io, {
10423
+ ok: true,
10424
+ dryRun: true,
10425
+ artifactId: command.artifactId,
10426
+ dashboardId: command.dashboardId,
10427
+ request: body,
10428
+ }, [`app_id: ${command.artifactId}`, `dashboard_id: ${command.dashboardId}`, 'dry_run: true']);
10429
+ return 0;
10430
+ }
10431
+ const credentials = await resolveDbCredentials(command, runtime);
10432
+ const payload = await requestAppDbDashboardApi({
10433
+ artifactId: command.artifactId,
10434
+ path: 'resources',
10435
+ dashboardId: command.dashboardId,
10436
+ method: 'POST',
10437
+ baseUrl: credentials.baseUrl,
10438
+ token: credentials.token,
10439
+ fetcher: runtime.fetcher ?? fetch,
10440
+ body,
10441
+ });
10442
+ printDbPayload(command, io, payload, [
10443
+ `app_id: ${command.artifactId}`,
10444
+ `dashboard_id: ${command.dashboardId}`,
10445
+ `resource_id: ${String(payload.resource?.id ?? '')}`,
10446
+ ]);
10447
+ return 0;
10448
+ }
10449
+ async function runDbWidgetsCreate(command, io, runtime) {
10450
+ const spec = await readJsonSpecFile({
10451
+ specPath: command.specPath,
10452
+ stdin: runtime.stdin,
10453
+ });
10454
+ const body = {
10455
+ title: command.title,
10456
+ spec,
10457
+ };
10458
+ if (command.dryRun) {
10459
+ printDbPayload(command, io, {
10460
+ ok: true,
10461
+ dryRun: true,
10462
+ artifactId: command.artifactId,
10463
+ dashboardId: command.dashboardId,
10464
+ request: body,
10465
+ }, [`app_id: ${command.artifactId}`, `dashboard_id: ${command.dashboardId}`, `title: ${command.title}`, 'dry_run: true']);
10466
+ return 0;
10467
+ }
10468
+ const credentials = await resolveDbCredentials(command, runtime);
10469
+ const payload = await requestAppDbDashboardApi({
10470
+ artifactId: command.artifactId,
10471
+ path: 'widgets',
10472
+ dashboardId: command.dashboardId,
10473
+ method: 'POST',
10474
+ baseUrl: credentials.baseUrl,
10475
+ token: credentials.token,
10476
+ fetcher: runtime.fetcher ?? fetch,
10477
+ body,
10478
+ });
10479
+ printDbPayload(command, io, payload, [
10480
+ `app_id: ${command.artifactId}`,
10481
+ `dashboard_id: ${command.dashboardId}`,
10482
+ `widget_id: ${String(payload.widget?.id ?? '')}`,
10483
+ ]);
10484
+ return 0;
10485
+ }
9758
10486
  async function runAnalyticsSummary(command, io, runtime) {
9759
10487
  const credentials = await resolveAnalyticsCredentials(command, runtime);
9760
10488
  const payload = await requestAnalyticsApi({
@@ -10466,6 +11194,21 @@ export async function runTenderCli(args, io = {
10466
11194
  else if (command.topic === 'app db query') {
10467
11195
  io.stdout(dbQueryHelp());
10468
11196
  }
11197
+ else if (command.topic === 'app db dashboards') {
11198
+ io.stdout(dbDashboardsHelp());
11199
+ }
11200
+ else if (command.topic === 'app db dashboards render') {
11201
+ io.stdout(dbDashboardsRenderHelp());
11202
+ }
11203
+ else if (command.topic === 'app db resources suggest') {
11204
+ io.stdout(dbResourcesSuggestHelp());
11205
+ }
11206
+ else if (command.topic === 'app db resources create') {
11207
+ io.stdout(dbResourcesCreateHelp());
11208
+ }
11209
+ else if (command.topic === 'app db widgets create') {
11210
+ io.stdout(dbWidgetsCreateHelp());
11211
+ }
10469
11212
  else if (command.topic === 'app') {
10470
11213
  io.stdout(appHelp());
10471
11214
  }
@@ -10657,6 +11400,21 @@ export async function runTenderCli(args, io = {
10657
11400
  if (command.command === 'app db query') {
10658
11401
  return await runDbQuery(command, io, runtime);
10659
11402
  }
11403
+ if (command.command === 'app db dashboards') {
11404
+ return await runDbDashboards(command, io, runtime);
11405
+ }
11406
+ if (command.command === 'app db resources suggest') {
11407
+ return await runDbResourcesSuggest(command, io, runtime);
11408
+ }
11409
+ if (command.command === 'app db dashboards render') {
11410
+ return await runDbDashboardsRender(command, io, runtime);
11411
+ }
11412
+ if (command.command === 'app db resources create') {
11413
+ return await runDbResourcesCreate(command, io, runtime);
11414
+ }
11415
+ if (command.command === 'app db widgets create') {
11416
+ return await runDbWidgetsCreate(command, io, runtime);
11417
+ }
10660
11418
  if (command.command === 'app git remote') {
10661
11419
  return await runGitRemote(command, io, runtime);
10662
11420
  }
@@ -10685,6 +11443,8 @@ export function shouldReadEntrypointStdin(args) {
10685
11443
  ((args[1] === 'git' && args[2] === 'credential') ||
10686
11444
  (args[1] === 'analytics' && hasFlagValue(args, '--spec', '-')) ||
10687
11445
  (args[1] === 'agent' && args[2] === 'heartbeat' && hasFlagValue(args, '--input', '-')) ||
11446
+ (args[1] === 'db' && (args[2] === 'resources' || args[2] === 'widgets') && hasFlagValue(args, '--spec', '-')) ||
11447
+ (args[1] === 'db' && args[2] === 'resources' && hasFlagValue(args, '--view', '-')) ||
10688
11448
  (args[1] === 'db' && args[2] === 'query' && hasFlagValue(args, '--file', '-'))));
10689
11449
  }
10690
11450
  async function readEntrypointStdin() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenderprompt/cli",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "tender": "bin/tender.js"