@tenderprompt/cli 0.1.10 → 0.1.12

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 +48 -3
  2. package/dist/index.js +931 -49
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -39,6 +39,14 @@ class TenderCliUsageError extends Error {
39
39
  this.name = "TenderCliUsageError";
40
40
  }
41
41
  }
42
+ class TenderCliAnalyticsApiError extends Error {
43
+ reasonCode;
44
+ constructor(message, reasonCode) {
45
+ super(message);
46
+ this.reasonCode = reasonCode;
47
+ this.name = "TenderCliAnalyticsApiError";
48
+ }
49
+ }
42
50
  const DEFAULT_PROFILE_NAME = "default";
43
51
  const DEFAULT_BASE_URL = "https://app.tenderprompt.com";
44
52
  const ARTIFACT_SOURCE_EXPORT_MAX_FILES = 1000;
@@ -199,6 +207,16 @@ Commands:
199
207
  List or create saved dashboards
200
208
  tender app analytics charts create <app-id>
201
209
  Create a saved chart from a chart spec
210
+ tender app analytics charts list <app-id>
211
+ List saved charts in one dashboard
212
+ tender app analytics charts update <app-id>
213
+ Update a saved chart title or spec
214
+ tender app analytics charts delete <app-id>
215
+ Delete a saved chart
216
+ tender app analytics properties list <app-id>
217
+ List properties available to dashboard queries
218
+ tender app analytics properties activate <app-id>
219
+ Make a custom event property queryable
202
220
  tender app context fetch <app-id>
203
221
  Fetch AGENTS.md, skills, and Tender App scaffold files
204
222
  tender app context status <app-id>
@@ -234,6 +252,11 @@ Examples:
234
252
  tender app analytics dashboards artifact_123 --json
235
253
  tender app analytics dashboards artifact_123 --create "Agent dashboard" --json
236
254
  tender app analytics charts create artifact_123 --dashboard analytics_dashboard_123 --title "Signup funnel" --spec funnel.json --json
255
+ tender app analytics charts list artifact_123 --dashboard analytics_dashboard_123 --json
256
+ tender app analytics charts update artifact_123 --chart analytics_chart_123 --title "Signup funnel" --spec funnel.json --dry-run --json
257
+ tender app analytics charts delete artifact_123 --chart analytics_chart_123 --dry-run --json
258
+ tender app analytics properties list artifact_123 --json
259
+ tender app analytics properties activate artifact_123 --event signup_started --property plan --type dimension --dry-run --json
237
260
  tender app context fetch artifact_123 --dir ./widget --dry-run --json
238
261
  tender app context status artifact_123 --dir ./widget --json
239
262
  tender app context refresh artifact_123 --dir ./widget --dry-run --json
@@ -268,6 +291,11 @@ Commands:
268
291
  tender app analytics export <kind> <app-id> Export catalog metadata or query rows
269
292
  tender app analytics charts templates Print reusable chart spec templates
270
293
  tender app analytics charts create <app-id> Create a saved chart
294
+ tender app analytics charts list <app-id> List saved charts in one dashboard
295
+ tender app analytics charts update <app-id> Update a saved chart
296
+ tender app analytics charts delete <app-id> Delete a saved chart
297
+ tender app analytics properties list <app-id> List queryable custom event properties
298
+ tender app analytics properties activate <app-id> Make a property queryable
271
299
 
272
300
  Examples:
273
301
  tender app analytics summary artifact_123 --range 30d --json
@@ -279,7 +307,12 @@ Examples:
279
307
  tender app analytics suggestions artifact_123 --range 2026-04-01..2026-04-30 --json
280
308
  tender app analytics dashboards artifact_123 --create "Agent dashboard" --json
281
309
  tender app analytics charts templates --json
282
- tender app analytics charts create artifact_123 --dashboard analytics_dashboard_123 --title "Signup funnel" --spec funnel.json --json`;
310
+ tender app analytics charts create artifact_123 --dashboard analytics_dashboard_123 --title "Signup funnel" --spec funnel.json --json
311
+ tender app analytics charts list artifact_123 --dashboard analytics_dashboard_123 --json
312
+ tender app analytics charts update artifact_123 --chart analytics_chart_123 --title "Signup funnel" --spec funnel.json --dry-run --json
313
+ tender app analytics charts delete artifact_123 --chart analytics_chart_123 --dry-run --json
314
+ tender app analytics properties list artifact_123 --json
315
+ tender app analytics properties activate artifact_123 --event signup_started --property plan --type dimension --dry-run --json`;
283
316
  }
284
317
  function analyticsSummaryHelp() {
285
318
  return `Usage: tender app analytics summary <app-id> [--range <7d|30d|90d|YYYY-MM-DD..YYYY-MM-DD>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
@@ -355,16 +388,59 @@ Examples:
355
388
  function analyticsChartsCreateHelp() {
356
389
  return `Usage: tender app analytics charts create <app-id> --dashboard <dashboard-id> --title <title> --spec <file|-> [--dry-run] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
357
390
 
391
+ Property charts automatically make required custom event properties queryable before
392
+ saving when the spec has an event_name = filter.
393
+
358
394
  Examples:
359
395
  tender app analytics charts create artifact_123 --dashboard analytics_dashboard_123 --title "Signup funnel" --spec funnel.json --dry-run --json
360
396
  tender app analytics charts create artifact_123 --dashboard analytics_dashboard_123 --title "Signup funnel" --spec funnel.json --json`;
361
397
  }
398
+ function analyticsChartsListHelp() {
399
+ return `Usage: tender app analytics charts list <app-id> --dashboard <dashboard-id> [--base-url <url>] [--token <token>] [--profile <name>] [--json]
400
+
401
+ Examples:
402
+ tender app analytics charts list artifact_123 --dashboard analytics_dashboard_123 --json`;
403
+ }
404
+ function analyticsChartsUpdateHelp() {
405
+ return `Usage: tender app analytics charts update <app-id> --chart <chart-id> [--title <title>] [--spec <file|->] [--dry-run] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
406
+
407
+ At least one of --title or --spec is required. When one is omitted, the CLI reads
408
+ the existing saved chart and keeps that value.
409
+ Property charts automatically make required custom event properties queryable before
410
+ saving when the spec has an event_name = filter.
411
+
412
+ Examples:
413
+ tender app analytics charts update artifact_123 --chart analytics_chart_123 --title "Signup funnel" --dry-run --json
414
+ tender app analytics charts update artifact_123 --chart analytics_chart_123 --spec funnel.json --dry-run --json
415
+ tender app analytics charts update artifact_123 --chart analytics_chart_123 --title "Signup funnel" --spec - --json < funnel.json`;
416
+ }
417
+ function analyticsChartsDeleteHelp() {
418
+ return `Usage: tender app analytics charts delete <app-id> --chart <chart-id> [--dry-run] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
419
+
420
+ Examples:
421
+ tender app analytics charts delete artifact_123 --chart analytics_chart_123 --dry-run --json
422
+ tender app analytics charts delete artifact_123 --chart analytics_chart_123 --json`;
423
+ }
424
+ function analyticsPropertiesListHelp() {
425
+ return `Usage: tender app analytics properties list <app-id> [--event <event-name>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
426
+
427
+ Examples:
428
+ tender app analytics properties list artifact_123 --json
429
+ tender app analytics properties list artifact_123 --event signup_started --json`;
430
+ }
431
+ function analyticsPropertiesActivateHelp() {
432
+ return `Usage: tender app analytics properties activate <app-id> --event <event-name> --property <property-key> --type <dimension|metric> [--label <label>] [--dry-run] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
433
+
434
+ Examples:
435
+ tender app analytics properties activate artifact_123 --event signup_started --property plan --type dimension --dry-run --json
436
+ tender app analytics properties activate artifact_123 --event score_saved --property score --type metric --json`;
437
+ }
362
438
  function artifactsListHelp() {
363
439
  return `Usage: tender app list [--base-url <url>] [--token <token>] [--profile <name>] [--json]
364
440
 
365
441
  Options:
366
442
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
367
- --token <token> Tender API token. Defaults to TENDER_API_TOKEN or the saved profile token.
443
+ --token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
368
444
  --profile <name> Local config profile name. Defaults to the saved default profile.
369
445
  --json Emit stable JSON for coding agents.
370
446
 
@@ -381,7 +457,7 @@ Options:
381
457
  --target-pack <id> Target pack id. Defaults to the API default.
382
458
  --target-pack-version <value> Target pack version. Defaults to the API default.
383
459
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
384
- --token <token> Tender API token. Defaults to TENDER_API_TOKEN or the saved profile token.
460
+ --token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
385
461
  --profile <name> Local config profile name. Defaults to the saved default profile.
386
462
  --json Emit stable JSON for coding agents.
387
463
 
@@ -395,7 +471,7 @@ function artifactsUpdateHelp() {
395
471
  Options:
396
472
  --name <name> New app name.
397
473
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
398
- --token <token> Tender API token. Defaults to TENDER_API_TOKEN or the saved profile token.
474
+ --token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
399
475
  --profile <name> Local config profile name. Defaults to the saved default profile.
400
476
  --json Emit stable JSON for coding agents.
401
477
 
@@ -409,7 +485,7 @@ Options:
409
485
  --dir <path> Local checkout directory to create/update. Defaults to current directory.
410
486
  --remote <name> Git remote name to add or update. Defaults to tender.
411
487
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
412
- --token <token> Tender API token. Defaults to TENDER_API_TOKEN or the saved profile token.
488
+ --token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
413
489
  --profile <name> Local config profile name. Defaults to the saved default profile.
414
490
  --ttl <value> Deprecated for Tender-hosted Git remotes; accepted for compatibility.
415
491
  --dry-run Show file and Git actions without writing.
@@ -427,7 +503,7 @@ function artifactsContextFetchHelp() {
427
503
  Options:
428
504
  --dir <path> Local checkout to update. Defaults to current directory.
429
505
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
430
- --token <token> Tender API token. Defaults to TENDER_API_TOKEN or the saved profile token.
506
+ --token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
431
507
  --profile <name> Local config profile name. Defaults to the saved default profile.
432
508
  --dry-run Show create/overwrite/conflict actions without writing files.
433
509
  --force Overwrite conflicting managed context files.
@@ -444,7 +520,7 @@ function artifactsContextStatusHelp() {
444
520
  Options:
445
521
  --dir <path> Local checkout to inspect. Defaults to current directory.
446
522
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
447
- --token <token> Tender API token. Defaults to TENDER_API_TOKEN or the saved profile token.
523
+ --token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
448
524
  --profile <name> Local config profile name. Defaults to the saved default profile.
449
525
  --json Emit stable JSON for coding agents.
450
526
 
@@ -457,7 +533,7 @@ function artifactsContextRefreshHelp() {
457
533
  Options:
458
534
  --dir <path> Local checkout to update. Defaults to current directory.
459
535
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
460
- --token <token> Tender API token. Defaults to TENDER_API_TOKEN or the saved profile token.
536
+ --token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
461
537
  --profile <name> Local config profile name. Defaults to the saved default profile.
462
538
  --dry-run Show create/overwrite/conflict actions without writing files.
463
539
  --force Overwrite conflicting managed context files.
@@ -514,7 +590,7 @@ function gitRemoteHelp() {
514
590
 
515
591
  Options:
516
592
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
517
- --token <token> Tender API token. Defaults to TENDER_API_TOKEN or the saved profile token.
593
+ --token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
518
594
  --profile <name> Local config profile name. Defaults to the saved default profile.
519
595
  --ttl <value> Deprecated for Tender-hosted Git remotes; accepted for compatibility.
520
596
  --json Emit stable JSON for coding agents.
@@ -531,7 +607,7 @@ Options:
531
607
  --dir <path> Local Git checkout to configure. Defaults to current directory.
532
608
  --remote <name> Git remote name to add or update. Defaults to tender.
533
609
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
534
- --token <token> Tender API token. Defaults to TENDER_API_TOKEN or the saved profile token.
610
+ --token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
535
611
  --profile <name> Local config profile name. Defaults to the saved default profile.
536
612
  --ttl <value> Deprecated for Tender-hosted Git remotes; accepted for compatibility.
537
613
  --dry-run Show Git commands without running them.
@@ -559,7 +635,7 @@ function previewRebuildHelp() {
559
635
  Options:
560
636
  --commit <sha> Rebuild preview only if the app workspace is at this Git commit.
561
637
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
562
- --token <token> Tender API token. Defaults to TENDER_API_TOKEN or the saved profile token.
638
+ --token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
563
639
  --profile <name> Local config profile name. Defaults to the saved default profile.
564
640
  --stream Request lifecycle events from the API.
565
641
  --json Emit stable JSON for coding agents.
@@ -573,7 +649,7 @@ function validateHelp() {
573
649
 
574
650
  Options:
575
651
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
576
- --token <token> Tender API token. Defaults to TENDER_API_TOKEN or the saved profile token.
652
+ --token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
577
653
  --profile <name> Local config profile name. Defaults to the saved default profile.
578
654
  --stream Request lifecycle events from the API.
579
655
  --json Emit stable JSON for coding agents.
@@ -588,7 +664,7 @@ function buildHelp() {
588
664
  Options:
589
665
  --commit <sha> Build only if the app workspace is at this Git commit.
590
666
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
591
- --token <token> Tender API token. Defaults to TENDER_API_TOKEN or the saved profile token.
667
+ --token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
592
668
  --profile <name> Local config profile name. Defaults to the saved default profile.
593
669
  --stream Request lifecycle events from the API.
594
670
  --json Emit stable JSON for coding agents.
@@ -604,7 +680,7 @@ Options:
604
680
  --commit <sha> Show the latest preview job for one artifact Git commit.
605
681
  --job <job-id> Show one lifecycle job directly.
606
682
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
607
- --token <token> Tender API token. Defaults to TENDER_API_TOKEN or the saved profile token.
683
+ --token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
608
684
  --profile <name> Local config profile name. Defaults to the saved default profile.
609
685
  --json Emit stable JSON for coding agents.
610
686
 
@@ -619,7 +695,7 @@ Options:
619
695
  --commit <sha> Watch the latest preview job for one artifact Git commit.
620
696
  --job <job-id> Watch one lifecycle job directly.
621
697
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
622
- --token <token> Tender API token. Defaults to TENDER_API_TOKEN or the saved profile token.
698
+ --token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
623
699
  --profile <name> Local config profile name. Defaults to the saved default profile.
624
700
  --stream Request Server-Sent Events from the API.
625
701
  --json Emit lifecycle events as JSON lines.
@@ -637,7 +713,7 @@ Options:
637
713
  --commit <sha> Publish only if the app workspace is at this Git commit.
638
714
  --message <text> Human audit message returned in CLI output.
639
715
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
640
- --token <token> Tender API token. Defaults to TENDER_API_TOKEN or the saved profile token.
716
+ --token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
641
717
  --profile <name> Local config profile name. Defaults to the saved default profile.
642
718
  --stream Request lifecycle events from the API.
643
719
  --json Emit stable JSON for coding agents.
@@ -652,7 +728,7 @@ function publishStatusHelp() {
652
728
  Options:
653
729
  --job <job-id> Show one publish lifecycle job directly.
654
730
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
655
- --token <token> Tender API token. Defaults to TENDER_API_TOKEN or the saved profile token.
731
+ --token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
656
732
  --profile <name> Local config profile name. Defaults to the saved default profile.
657
733
  --json Emit stable JSON for coding agents.
658
734
 
@@ -665,7 +741,7 @@ function publishWatchHelp() {
665
741
  Options:
666
742
  --job <job-id> Watch one publish lifecycle job directly.
667
743
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
668
- --token <token> Tender API token. Defaults to TENDER_API_TOKEN or the saved profile token.
744
+ --token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
669
745
  --profile <name> Local config profile name. Defaults to the saved default profile.
670
746
  --stream Request Server-Sent Events from the API.
671
747
  --json Emit lifecycle events as JSON lines.
@@ -740,12 +816,20 @@ function tenderCliCapabilities() {
740
816
  "tender app analytics charts templates --json",
741
817
  "tender app analytics query <artifact-id> --spec chart.json --json",
742
818
  "tender app analytics export query <artifact-id> --spec chart.json --format csv",
819
+ "tender app analytics properties list <artifact-id> --json",
820
+ "tender app analytics properties activate <artifact-id> --event <event-name> --property <property-key> --type dimension --dry-run --json",
743
821
  "tender app analytics dashboards <artifact-id> --create \"Agent dashboard\" --dry-run --json",
744
822
  "tender app analytics charts create <artifact-id> --dashboard <dashboard-id> --title <title> --spec chart.json --dry-run --json",
823
+ "tender app analytics charts list <artifact-id> --dashboard <dashboard-id> --json",
824
+ "tender app analytics charts update <artifact-id> --chart <chart-id> --title <title> --spec chart.json --dry-run --json",
825
+ "tender app analytics charts delete <artifact-id> --chart <chart-id> --dry-run --json",
745
826
  ],
746
827
  notes: [
747
828
  "Combine CLI capabilities with local source inspection of analytics calls.",
748
- "Validate chart specs with query before saving charts.",
829
+ "Validate chart specs with query before creating or updating saved charts.",
830
+ "Property chart create/update commands automatically make missing custom event properties queryable when the spec has an event_name = filter.",
831
+ "Ad hoc query is read-only; if it reports a missing dashboard-query property, activate the property first or save the chart through charts create/update.",
832
+ "Use list before writes so agents can avoid duplicates and explain the exact chart they are changing.",
749
833
  ],
750
834
  },
751
835
  {
@@ -801,6 +885,46 @@ function tenderCliCapabilities() {
801
885
  requiresAuth: false,
802
886
  writes: false,
803
887
  },
888
+ {
889
+ command: "tender app analytics properties list <artifact-id> --json",
890
+ purpose: "List custom event properties available to dashboard queries.",
891
+ requiresAuth: true,
892
+ writes: false,
893
+ },
894
+ {
895
+ command: "tender app analytics properties activate <artifact-id> --event <event-name> --property <property-key> --type <dimension|metric> --dry-run --json",
896
+ purpose: "Preview or activate the custom event property needed for a property group-by or property metric chart.",
897
+ requiresAuth: true,
898
+ writes: true,
899
+ dryRun: true,
900
+ },
901
+ {
902
+ command: "tender app analytics charts create <artifact-id> --dashboard <dashboard-id> --title <title> --spec chart.json --dry-run --json",
903
+ purpose: "Preview or create a saved chart from a validated chart spec.",
904
+ requiresAuth: true,
905
+ writes: true,
906
+ dryRun: true,
907
+ },
908
+ {
909
+ command: "tender app analytics charts list <artifact-id> --dashboard <dashboard-id> --json",
910
+ purpose: "List saved charts and full chart specs from one dashboard.",
911
+ requiresAuth: true,
912
+ writes: false,
913
+ },
914
+ {
915
+ command: "tender app analytics charts update <artifact-id> --chart <chart-id> --title <title> --spec chart.json --dry-run --json",
916
+ purpose: "Preview or update a saved chart title and chart spec.",
917
+ requiresAuth: true,
918
+ writes: true,
919
+ dryRun: true,
920
+ },
921
+ {
922
+ command: "tender app analytics charts delete <artifact-id> --chart <chart-id> --dry-run --json",
923
+ purpose: "Preview or delete a saved chart.",
924
+ requiresAuth: true,
925
+ writes: true,
926
+ dryRun: true,
927
+ },
804
928
  ],
805
929
  security: [
806
930
  "Prefer artifact-scoped device tokens.",
@@ -921,6 +1045,19 @@ async function resolveApiCredentials(input) {
921
1045
  profileName: null,
922
1046
  };
923
1047
  }
1048
+ if (input.profileName) {
1049
+ const config = await readConfig(input.env);
1050
+ const profile = config.profiles?.[input.profileName];
1051
+ if (!profile) {
1052
+ throw new TenderCliUsageError(`Tender profile "${input.profileName}" was not found. Run \`tender auth login --device --save-profile ${input.profileName}\` or choose another --profile.`);
1053
+ }
1054
+ return {
1055
+ baseUrl: input.baseUrl ?? profile.baseUrl ?? readDefaultBaseUrl(input.env),
1056
+ token: profile.token,
1057
+ source: "config",
1058
+ profileName: input.profileName,
1059
+ };
1060
+ }
924
1061
  if (input.env.TENDER_API_TOKEN) {
925
1062
  return {
926
1063
  baseUrl: input.baseUrl ?? readDefaultBaseUrl(input.env),
@@ -930,7 +1067,7 @@ async function resolveApiCredentials(input) {
930
1067
  };
931
1068
  }
932
1069
  const config = await readConfig(input.env);
933
- const profileName = input.profileName ?? config.defaultProfile ?? DEFAULT_PROFILE_NAME;
1070
+ const profileName = config.defaultProfile ?? DEFAULT_PROFILE_NAME;
934
1071
  const profile = config.profiles?.[profileName];
935
1072
  if (!profile) {
936
1073
  throw new TenderCliUsageError("Tender API token is required. Run `tender auth login --device`, set TENDER_API_TOKEN, or pass --token <token>.");
@@ -2801,13 +2938,13 @@ function analyticsChartTemplates() {
2801
2938
  id: "property_breakdown",
2802
2939
  title: "Property breakdown",
2803
2940
  visualization: "bar",
2804
- purpose: "Compare a promoted dimension property observed in events.",
2941
+ purpose: "Compare a queryable dimension property observed in events.",
2805
2942
  usableAsIs: false,
2806
2943
  placeholders: {
2807
- property: "<promoted_dimension_property>",
2944
+ property: "<queryable_dimension_property>",
2808
2945
  },
2809
2946
  adaptWith: [
2810
- "Use a property key promoted by capabilities chartSpec.groupBy, such as property:plan.",
2947
+ "Use a queryable property key from capabilities chartSpec.groupBy, such as property:plan.",
2811
2948
  "Choose stable categorical properties with enough observations to compare.",
2812
2949
  ],
2813
2950
  spec: {
@@ -2815,7 +2952,7 @@ function analyticsChartTemplates() {
2815
2952
  dataset: "events",
2816
2953
  visualization: "bar",
2817
2954
  metric: { name: "custom_event_count" },
2818
- groupBy: [{ field: "property:<promoted_dimension_property>" }],
2955
+ groupBy: [{ field: "property:<queryable_dimension_property>" }],
2819
2956
  filters: [],
2820
2957
  range: { preset: "30d" },
2821
2958
  sort: [{ field: "value", direction: "desc" }],
@@ -2829,17 +2966,17 @@ function analyticsChartTemplates() {
2829
2966
  purpose: "Track an observed numeric event property over time.",
2830
2967
  usableAsIs: false,
2831
2968
  placeholders: {
2832
- metric: "avg:<promoted_metric_property>",
2969
+ metric: "avg:<queryable_metric_property>",
2833
2970
  },
2834
2971
  adaptWith: [
2835
- "Use a numeric property key promoted by capabilities chartSpec.metrics.",
2972
+ "Use a queryable numeric property key from capabilities chartSpec.metrics.",
2836
2973
  "Prefer averages for scores, durations, quantities, or ordinal values.",
2837
2974
  ],
2838
2975
  spec: {
2839
2976
  version: 1,
2840
2977
  dataset: "events",
2841
2978
  visualization: "line",
2842
- metric: { name: "avg:<promoted_metric_property>" },
2979
+ metric: { name: "avg:<queryable_metric_property>" },
2843
2980
  groupBy: [{ field: "day" }],
2844
2981
  filters: [],
2845
2982
  range: { preset: "30d" },
@@ -2882,7 +3019,7 @@ function runAnalyticsChartsTemplates(command, io) {
2882
3019
  templates,
2883
3020
  next: [
2884
3021
  "Run tender app analytics capabilities <artifact-id> --include-catalog --range 30d --json.",
2885
- "Choose a template and replace placeholders with observed events, paths, or promoted property keys.",
3022
+ "Choose a template and replace placeholders with observed events, paths, or queryable property keys.",
2886
3023
  "Validate with tender app analytics query <artifact-id> --spec chart.json --json.",
2887
3024
  "Preview saved writes with tender app analytics dashboards ... --dry-run and charts create ... --dry-run.",
2888
3025
  ],
@@ -2953,6 +3090,208 @@ function parseAnalyticsChartsCreateArgs(args) {
2953
3090
  ...parsed,
2954
3091
  };
2955
3092
  }
3093
+ function parseAnalyticsChartsListArgs(args) {
3094
+ if (args.includes("--help") || args.includes("-h")) {
3095
+ return { command: "help", topic: "app analytics charts list" };
3096
+ }
3097
+ const artifactId = args[0];
3098
+ if (!artifactId || artifactId.startsWith("-")) {
3099
+ throw new TenderCliUsageError("app id is required. Example: tender app analytics charts list <app-id> --dashboard <dashboard-id> --json");
3100
+ }
3101
+ const parsed = parseAnalyticsCommonArgs(args, 1);
3102
+ let dashboardId = null;
3103
+ for (let index = 0; index < parsed.remaining.length; index += 1) {
3104
+ const arg = parsed.remaining[index];
3105
+ if (arg === "--dashboard") {
3106
+ dashboardId = parseRequiredFlagValue(parsed.remaining[index + 1], "--dashboard");
3107
+ index += 1;
3108
+ continue;
3109
+ }
3110
+ throw new TenderCliUsageError(`Unknown analytics charts list option: ${arg}`);
3111
+ }
3112
+ if (!dashboardId) {
3113
+ throw new TenderCliUsageError("--dashboard is required. Example: tender app analytics charts list artifact_123 --dashboard analytics_dashboard_123 --json");
3114
+ }
3115
+ return {
3116
+ command: "app analytics charts list",
3117
+ artifactId,
3118
+ dashboardId,
3119
+ ...parsed,
3120
+ };
3121
+ }
3122
+ function parseAnalyticsChartsUpdateArgs(args) {
3123
+ if (args.includes("--help") || args.includes("-h")) {
3124
+ return { command: "help", topic: "app analytics charts update" };
3125
+ }
3126
+ const artifactId = args[0];
3127
+ if (!artifactId || artifactId.startsWith("-")) {
3128
+ throw new TenderCliUsageError("app id is required. Example: tender app analytics charts update <app-id> --chart <chart-id> --title <title> --spec chart.json --json");
3129
+ }
3130
+ const parsed = parseAnalyticsCommonArgs(args, 1);
3131
+ let chartId = null;
3132
+ let title = null;
3133
+ let specPath = null;
3134
+ let dryRun = false;
3135
+ for (let index = 0; index < parsed.remaining.length; index += 1) {
3136
+ const arg = parsed.remaining[index];
3137
+ if (arg === "--chart") {
3138
+ chartId = parseRequiredFlagValue(parsed.remaining[index + 1], "--chart");
3139
+ index += 1;
3140
+ continue;
3141
+ }
3142
+ if (arg === "--title") {
3143
+ title = parseRequiredFlagValue(parsed.remaining[index + 1], "--title");
3144
+ index += 1;
3145
+ continue;
3146
+ }
3147
+ if (arg === "--spec") {
3148
+ specPath = parseSpecPath(parsed.remaining[index + 1]);
3149
+ index += 1;
3150
+ continue;
3151
+ }
3152
+ if (arg === "--dry-run") {
3153
+ dryRun = true;
3154
+ continue;
3155
+ }
3156
+ throw new TenderCliUsageError(`Unknown analytics charts update option: ${arg}`);
3157
+ }
3158
+ if (!chartId) {
3159
+ throw new TenderCliUsageError("--chart is required. Example: tender app analytics charts update artifact_123 --chart analytics_chart_123 --title \"Signup funnel\" --json");
3160
+ }
3161
+ if (!title && !specPath) {
3162
+ throw new TenderCliUsageError("At least one of --title or --spec is required. Example: tender app analytics charts update artifact_123 --chart analytics_chart_123 --title \"Signup funnel\" --dry-run --json");
3163
+ }
3164
+ return {
3165
+ command: "app analytics charts update",
3166
+ artifactId,
3167
+ chartId,
3168
+ title,
3169
+ specPath,
3170
+ dryRun,
3171
+ ...parsed,
3172
+ };
3173
+ }
3174
+ function parseAnalyticsChartsDeleteArgs(args) {
3175
+ if (args.includes("--help") || args.includes("-h")) {
3176
+ return { command: "help", topic: "app analytics charts delete" };
3177
+ }
3178
+ const artifactId = args[0];
3179
+ if (!artifactId || artifactId.startsWith("-")) {
3180
+ throw new TenderCliUsageError("app id is required. Example: tender app analytics charts delete <app-id> --chart <chart-id> --dry-run --json");
3181
+ }
3182
+ const parsed = parseAnalyticsCommonArgs(args, 1);
3183
+ let chartId = null;
3184
+ let dryRun = false;
3185
+ for (let index = 0; index < parsed.remaining.length; index += 1) {
3186
+ const arg = parsed.remaining[index];
3187
+ if (arg === "--chart") {
3188
+ chartId = parseRequiredFlagValue(parsed.remaining[index + 1], "--chart");
3189
+ index += 1;
3190
+ continue;
3191
+ }
3192
+ if (arg === "--dry-run") {
3193
+ dryRun = true;
3194
+ continue;
3195
+ }
3196
+ throw new TenderCliUsageError(`Unknown analytics charts delete option: ${arg}`);
3197
+ }
3198
+ if (!chartId) {
3199
+ throw new TenderCliUsageError("--chart is required. Example: tender app analytics charts delete artifact_123 --chart analytics_chart_123 --dry-run --json");
3200
+ }
3201
+ return {
3202
+ command: "app analytics charts delete",
3203
+ artifactId,
3204
+ chartId,
3205
+ dryRun,
3206
+ ...parsed,
3207
+ };
3208
+ }
3209
+ function parseAnalyticsPropertiesListArgs(args) {
3210
+ if (args.includes("--help") || args.includes("-h")) {
3211
+ return { command: "help", topic: "app analytics properties list" };
3212
+ }
3213
+ const artifactId = args[0];
3214
+ if (!artifactId || artifactId.startsWith("-")) {
3215
+ throw new TenderCliUsageError("app id is required. Example: tender app analytics properties list <app-id> --json");
3216
+ }
3217
+ const parsed = parseAnalyticsCommonArgs(args, 1);
3218
+ let eventName = null;
3219
+ for (let index = 0; index < parsed.remaining.length; index += 1) {
3220
+ const arg = parsed.remaining[index];
3221
+ if (arg === "--event") {
3222
+ eventName = parseRequiredFlagValue(parsed.remaining[index + 1], "--event");
3223
+ index += 1;
3224
+ continue;
3225
+ }
3226
+ throw new TenderCliUsageError(`Unknown analytics properties list option: ${arg}`);
3227
+ }
3228
+ return {
3229
+ command: "app analytics properties list",
3230
+ artifactId,
3231
+ eventName,
3232
+ ...parsed,
3233
+ };
3234
+ }
3235
+ function parseAnalyticsPropertiesActivateArgs(args) {
3236
+ if (args.includes("--help") || args.includes("-h")) {
3237
+ return { command: "help", topic: "app analytics properties activate" };
3238
+ }
3239
+ const artifactId = args[0];
3240
+ if (!artifactId || artifactId.startsWith("-")) {
3241
+ throw new TenderCliUsageError("app id is required. Example: tender app analytics properties activate <app-id> --event <event-name> --property <property-key> --type dimension --json");
3242
+ }
3243
+ const parsed = parseAnalyticsCommonArgs(args, 1);
3244
+ let eventName = null;
3245
+ let propertyKey = null;
3246
+ let propertyType = null;
3247
+ let label = null;
3248
+ let dryRun = false;
3249
+ for (let index = 0; index < parsed.remaining.length; index += 1) {
3250
+ const arg = parsed.remaining[index];
3251
+ if (arg === "--event") {
3252
+ eventName = parseRequiredFlagValue(parsed.remaining[index + 1], "--event");
3253
+ index += 1;
3254
+ continue;
3255
+ }
3256
+ if (arg === "--property") {
3257
+ propertyKey = parseRequiredFlagValue(parsed.remaining[index + 1], "--property");
3258
+ index += 1;
3259
+ continue;
3260
+ }
3261
+ if (arg === "--type") {
3262
+ const value = parseRequiredFlagValue(parsed.remaining[index + 1], "--type");
3263
+ if (value !== "dimension" && value !== "metric") {
3264
+ throw new TenderCliUsageError("--type must be dimension or metric.");
3265
+ }
3266
+ propertyType = value;
3267
+ index += 1;
3268
+ continue;
3269
+ }
3270
+ if (arg === "--label") {
3271
+ label = parseRequiredFlagValue(parsed.remaining[index + 1], "--label");
3272
+ index += 1;
3273
+ continue;
3274
+ }
3275
+ if (arg === "--dry-run") {
3276
+ dryRun = true;
3277
+ continue;
3278
+ }
3279
+ throw new TenderCliUsageError(`Unknown analytics properties activate option: ${arg}`);
3280
+ }
3281
+ if (!eventName || !propertyKey || !propertyType) {
3282
+ throw new TenderCliUsageError("--event, --property, and --type are required. Example: tender app analytics properties activate artifact_123 --event signup_started --property plan --type dimension --json");
3283
+ }
3284
+ return {
3285
+ command: "app analytics properties activate",
3286
+ artifactId,
3287
+ eventName,
3288
+ propertyKey,
3289
+ propertyType,
3290
+ label,
3291
+ dryRun,
3292
+ ...parsed,
3293
+ };
3294
+ }
2956
3295
  function parseTenderArgs(args) {
2957
3296
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
2958
3297
  return { command: "help", topic: "root" };
@@ -3036,6 +3375,23 @@ function parseTenderArgs(args) {
3036
3375
  if (resourceArgs[1] === "analytics" && resourceArgs[2] === "dashboards") {
3037
3376
  return parseAnalyticsDashboardsArgs(resourceArgs.slice(3));
3038
3377
  }
3378
+ if (resourceArgs[1] === "analytics" &&
3379
+ resourceArgs[2] === "properties" &&
3380
+ (resourceArgs[3] === undefined ||
3381
+ resourceArgs[3] === "--help" ||
3382
+ resourceArgs[3] === "-h")) {
3383
+ return { command: "help", topic: "app analytics properties list" };
3384
+ }
3385
+ if (resourceArgs[1] === "analytics" &&
3386
+ resourceArgs[2] === "properties" &&
3387
+ resourceArgs[3] === "list") {
3388
+ return parseAnalyticsPropertiesListArgs(resourceArgs.slice(4));
3389
+ }
3390
+ if (resourceArgs[1] === "analytics" &&
3391
+ resourceArgs[2] === "properties" &&
3392
+ resourceArgs[3] === "activate") {
3393
+ return parseAnalyticsPropertiesActivateArgs(resourceArgs.slice(4));
3394
+ }
3039
3395
  if (resourceArgs[1] === "analytics" &&
3040
3396
  resourceArgs[2] === "export" &&
3041
3397
  (resourceArgs[3] === undefined ||
@@ -3070,6 +3426,21 @@ function parseTenderArgs(args) {
3070
3426
  resourceArgs[3] === "create") {
3071
3427
  return parseAnalyticsChartsCreateArgs(resourceArgs.slice(4));
3072
3428
  }
3429
+ if (resourceArgs[1] === "analytics" &&
3430
+ resourceArgs[2] === "charts" &&
3431
+ resourceArgs[3] === "list") {
3432
+ return parseAnalyticsChartsListArgs(resourceArgs.slice(4));
3433
+ }
3434
+ if (resourceArgs[1] === "analytics" &&
3435
+ resourceArgs[2] === "charts" &&
3436
+ resourceArgs[3] === "update") {
3437
+ return parseAnalyticsChartsUpdateArgs(resourceArgs.slice(4));
3438
+ }
3439
+ if (resourceArgs[1] === "analytics" &&
3440
+ resourceArgs[2] === "charts" &&
3441
+ resourceArgs[3] === "delete") {
3442
+ return parseAnalyticsChartsDeleteArgs(resourceArgs.slice(4));
3443
+ }
3073
3444
  if (resourceArgs[1] === "context" &&
3074
3445
  (resourceArgs[2] === undefined ||
3075
3446
  resourceArgs[2] === "--help" ||
@@ -3442,7 +3813,9 @@ async function requestAnalyticsApi(input) {
3442
3813
  if (input.includeCatalog) {
3443
3814
  params.set("includeCatalog", "1");
3444
3815
  }
3445
- const suffix = input.analyticsPath ? `/${input.analyticsPath}` : "";
3816
+ const suffix = input.analyticsPath
3817
+ ? `/${input.analyticsPath}${input.resourceId ? `/${encodeURIComponent(input.resourceId)}` : ""}`
3818
+ : "";
3446
3819
  const query = params.size > 0 ? `?${params.toString()}` : "";
3447
3820
  const response = await input.fetcher(`${input.baseUrl}/api/v1/artifacts/${encodeURIComponent(input.artifactId)}/analytics${suffix}${query}`, {
3448
3821
  method: input.method,
@@ -3460,12 +3833,12 @@ async function requestAnalyticsApi(input) {
3460
3833
  const message = (typeof payload?.message === "string" && payload.message) ||
3461
3834
  (typeof payload?.reasonCode === "string" && payload.reasonCode) ||
3462
3835
  `Analytics request failed with HTTP ${response.status}.`;
3463
- throw new Error([
3836
+ throw new TenderCliAnalyticsApiError([
3464
3837
  message,
3465
3838
  `Scope: artifact ${input.artifactId}, analytics/${input.analyticsPath || "summary"}.`,
3466
3839
  `Reason: ${reasonCode}.`,
3467
3840
  `Example: ${analyticsCorrectedExample(input.artifactId, input.analyticsPath)}.`,
3468
- ].join("\n"));
3841
+ ].join("\n"), reasonCode);
3469
3842
  }
3470
3843
  return payload;
3471
3844
  }
@@ -3479,6 +3852,9 @@ function analyticsCorrectedExample(artifactId, analyticsPath) {
3479
3852
  if (analyticsPath === "suggestions") {
3480
3853
  return `tender app analytics suggestions ${artifactId} --range 30d --json`;
3481
3854
  }
3855
+ if (analyticsPath === "properties") {
3856
+ return `tender app analytics properties list ${artifactId} --json`;
3857
+ }
3482
3858
  if (analyticsPath === "dashboards") {
3483
3859
  return `tender app analytics dashboards ${artifactId} --json`;
3484
3860
  }
@@ -3487,6 +3863,250 @@ function analyticsCorrectedExample(artifactId, analyticsPath) {
3487
3863
  }
3488
3864
  return `tender app analytics summary ${artifactId} --range 30d --json`;
3489
3865
  }
3866
+ function isCliJsonRecord(value) {
3867
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3868
+ }
3869
+ function readCliStringField(record, field) {
3870
+ const value = record[field];
3871
+ return typeof value === "string" ? value : null;
3872
+ }
3873
+ function readCliBooleanField(record, field) {
3874
+ const value = record[field];
3875
+ return typeof value === "boolean" ? value : false;
3876
+ }
3877
+ function readAnalyticsDashboardRecords(payload) {
3878
+ return Array.isArray(payload.dashboards)
3879
+ ? payload.dashboards.filter(isCliJsonRecord)
3880
+ : [];
3881
+ }
3882
+ function readAnalyticsChartRecords(dashboard) {
3883
+ return Array.isArray(dashboard.charts)
3884
+ ? dashboard.charts.filter(isCliJsonRecord)
3885
+ : [];
3886
+ }
3887
+ function findAnalyticsDashboard(payload, dashboardId) {
3888
+ return (readAnalyticsDashboardRecords(payload).find((dashboard) => readCliStringField(dashboard, "id") === dashboardId) ?? null);
3889
+ }
3890
+ function findAnalyticsChart(payload, chartId) {
3891
+ for (const dashboard of readAnalyticsDashboardRecords(payload)) {
3892
+ const chart = readAnalyticsChartRecords(dashboard).find((candidate) => readCliStringField(candidate, "id") === chartId);
3893
+ if (chart) {
3894
+ return { dashboard, chart };
3895
+ }
3896
+ }
3897
+ return null;
3898
+ }
3899
+ function readAnalyticsChartTitle(chart) {
3900
+ return readCliStringField(chart, "title") ?? "Analytics chart";
3901
+ }
3902
+ function readAnalyticsChartSpec(chart) {
3903
+ const spec = chart.spec;
3904
+ if (spec === undefined) {
3905
+ throw new Error("Saved chart does not include a chart spec.");
3906
+ }
3907
+ return spec;
3908
+ }
3909
+ function readAnalyticsChartLayout(chart) {
3910
+ return isCliJsonRecord(chart.layout) ? chart.layout : {};
3911
+ }
3912
+ function readAnalyticsPropertyRecords(payload) {
3913
+ return Array.isArray(payload.properties)
3914
+ ? payload.properties.filter(isCliJsonRecord)
3915
+ : [];
3916
+ }
3917
+ function readAnalyticsSpecEventNameFilter(spec) {
3918
+ if (!isCliJsonRecord(spec) || !Array.isArray(spec.filters)) {
3919
+ return null;
3920
+ }
3921
+ const filter = spec.filters.filter(isCliJsonRecord).find((candidate) => readCliStringField(candidate, "field") === "event_name" &&
3922
+ readCliStringField(candidate, "op") === "=" &&
3923
+ typeof candidate.value === "string" &&
3924
+ candidate.value.trim().length > 0);
3925
+ return typeof filter?.value === "string" ? filter.value.trim() : null;
3926
+ }
3927
+ function readAnalyticsSpecMetricName(spec) {
3928
+ if (!isCliJsonRecord(spec) || !isCliJsonRecord(spec.metric)) {
3929
+ return null;
3930
+ }
3931
+ return readCliStringField(spec.metric, "name");
3932
+ }
3933
+ function readAnalyticsSpecGroupField(spec) {
3934
+ if (!isCliJsonRecord(spec) || !Array.isArray(spec.groupBy)) {
3935
+ return null;
3936
+ }
3937
+ const [group] = spec.groupBy.filter(isCliJsonRecord);
3938
+ return group ? readCliStringField(group, "field") : null;
3939
+ }
3940
+ function readRequiredAnalyticsPropertiesFromSpec(spec) {
3941
+ const eventName = readAnalyticsSpecEventNameFilter(spec);
3942
+ const requirements = [];
3943
+ const groupField = readAnalyticsSpecGroupField(spec);
3944
+ if (groupField?.startsWith("property:")) {
3945
+ if (!eventName) {
3946
+ throw new TenderCliUsageError("Property group-by chart specs require an event_name = filter so the CLI can make the property available to dashboard queries.");
3947
+ }
3948
+ requirements.push({
3949
+ eventName,
3950
+ propertyKey: groupField.slice("property:".length),
3951
+ propertyType: "dimension",
3952
+ });
3953
+ }
3954
+ const metricName = readAnalyticsSpecMetricName(spec);
3955
+ if (metricName?.startsWith("sum:") || metricName?.startsWith("avg:")) {
3956
+ if (!eventName) {
3957
+ throw new TenderCliUsageError("Property metric chart specs require an event_name = filter so the CLI can make the property available to dashboard queries.");
3958
+ }
3959
+ requirements.push({
3960
+ eventName,
3961
+ propertyKey: metricName.slice(metricName.indexOf(":") + 1),
3962
+ propertyType: "metric",
3963
+ });
3964
+ }
3965
+ const seen = new Set();
3966
+ return requirements.filter((requirement) => {
3967
+ const key = `${requirement.propertyType}:${requirement.eventName}:${requirement.propertyKey}`;
3968
+ if (seen.has(key)) {
3969
+ return false;
3970
+ }
3971
+ seen.add(key);
3972
+ return true;
3973
+ });
3974
+ }
3975
+ async function ensureAnalyticsProperties(input) {
3976
+ if (input.requirements.length === 0) {
3977
+ return [];
3978
+ }
3979
+ const plan = [];
3980
+ for (const requirement of input.requirements) {
3981
+ const body = {
3982
+ eventName: requirement.eventName,
3983
+ propertyKey: requirement.propertyKey,
3984
+ propertyType: requirement.propertyType,
3985
+ label: requirement.propertyKey,
3986
+ dryRun: input.dryRun,
3987
+ };
3988
+ const payload = await requestAnalyticsApi({
3989
+ artifactId: input.command.artifactId,
3990
+ analyticsPath: "properties",
3991
+ method: "POST",
3992
+ baseUrl: input.credentials.baseUrl,
3993
+ token: input.credentials.token,
3994
+ fetcher: input.runtime.fetcher ?? fetch,
3995
+ body,
3996
+ });
3997
+ plan.push({
3998
+ eventName: requirement.eventName,
3999
+ propertyKey: requirement.propertyKey,
4000
+ propertyType: requirement.propertyType,
4001
+ label: requirement.propertyKey,
4002
+ status: readCliBooleanField(payload, "alreadyActive")
4003
+ ? "existing"
4004
+ : input.dryRun
4005
+ ? "planned"
4006
+ : "activated",
4007
+ property: payload.property,
4008
+ });
4009
+ }
4010
+ return plan;
4011
+ }
4012
+ function analyticsCommandAuthFlags(command) {
4013
+ const flags = [];
4014
+ if (command.baseUrl) {
4015
+ flags.push("--base-url", shellQuote(command.baseUrl));
4016
+ }
4017
+ if (command.profileName) {
4018
+ flags.push("--profile", shellQuote(command.profileName));
4019
+ }
4020
+ return flags;
4021
+ }
4022
+ function analyticsPropertyActivationCommand(input) {
4023
+ return [
4024
+ "tender",
4025
+ "app",
4026
+ "analytics",
4027
+ "properties",
4028
+ "activate",
4029
+ shellQuote(input.command.artifactId),
4030
+ "--event",
4031
+ shellQuote(input.requirement.eventName),
4032
+ "--property",
4033
+ shellQuote(input.requirement.propertyKey),
4034
+ "--type",
4035
+ input.requirement.propertyType,
4036
+ ...(input.dryRun ? ["--dry-run"] : []),
4037
+ ...analyticsCommandAuthFlags(input.command),
4038
+ "--json",
4039
+ ].join(" ");
4040
+ }
4041
+ function createAnalyticsPropertyGuidance(input) {
4042
+ let requirements;
4043
+ try {
4044
+ requirements = readRequiredAnalyticsPropertiesFromSpec(input.spec);
4045
+ }
4046
+ catch {
4047
+ return null;
4048
+ }
4049
+ if (requirements.length === 0) {
4050
+ return null;
4051
+ }
4052
+ const lines = [
4053
+ "To make this chart available for dashboard queries, activate the custom event property first:",
4054
+ ];
4055
+ for (const requirement of requirements) {
4056
+ lines.push(analyticsPropertyActivationCommand({
4057
+ command: input.command,
4058
+ requirement,
4059
+ dryRun: true,
4060
+ }), analyticsPropertyActivationCommand({
4061
+ command: input.command,
4062
+ requirement,
4063
+ dryRun: false,
4064
+ }));
4065
+ }
4066
+ if (input.command.flagToken) {
4067
+ lines.push("Pass the same --token again, or use a saved profile/TENDER_API_TOKEN; tokens are not echoed in suggestions.");
4068
+ }
4069
+ lines.push("Then rerun the original analytics query.");
4070
+ return lines.join("\n");
4071
+ }
4072
+ async function requestAnalyticsQueryWithPropertyGuidance(input) {
4073
+ try {
4074
+ return await requestAnalyticsApi({
4075
+ artifactId: input.command.artifactId,
4076
+ analyticsPath: "query",
4077
+ method: "POST",
4078
+ baseUrl: input.credentials.baseUrl,
4079
+ token: input.credentials.token,
4080
+ fetcher: input.runtime.fetcher ?? fetch,
4081
+ body: { spec: input.spec },
4082
+ });
4083
+ }
4084
+ catch (error) {
4085
+ if (error instanceof TenderCliAnalyticsApiError &&
4086
+ error.reasonCode === "analytics_property_not_queryable") {
4087
+ const guidance = createAnalyticsPropertyGuidance({
4088
+ command: input.command,
4089
+ spec: input.spec,
4090
+ });
4091
+ if (guidance) {
4092
+ throw new TenderCliAnalyticsApiError(`${error.message}\n\n${guidance}`, error.reasonCode);
4093
+ }
4094
+ }
4095
+ throw error;
4096
+ }
4097
+ }
4098
+ async function requestAnalyticsDashboardsForCli(command, runtime) {
4099
+ const credentials = await resolveAnalyticsCredentials(command, runtime);
4100
+ const payload = await requestAnalyticsApi({
4101
+ artifactId: command.artifactId,
4102
+ analyticsPath: "dashboards",
4103
+ method: "GET",
4104
+ baseUrl: credentials.baseUrl,
4105
+ token: credentials.token,
4106
+ fetcher: runtime.fetcher ?? fetch,
4107
+ });
4108
+ return { credentials, payload };
4109
+ }
3490
4110
  function printAnalyticsPayload(command, io, payload, fallbackLines) {
3491
4111
  if (command.json) {
3492
4112
  io.stdout(JSON.stringify(payload, null, 2));
@@ -3775,6 +4395,47 @@ async function planSourceFiles(input) {
3775
4395
  }
3776
4396
  async function runAuthStatus(command, io, runtime) {
3777
4397
  const env = runtime.env ?? process.env;
4398
+ if (command.profileName) {
4399
+ const config = await readConfig(env);
4400
+ const profile = config.profiles?.[command.profileName] ?? null;
4401
+ const result = {
4402
+ ok: Boolean(profile),
4403
+ command: command.command,
4404
+ source: profile ? "config" : null,
4405
+ profile: command.profileName,
4406
+ configPath: readConfigPath(env),
4407
+ baseUrl: profile?.baseUrl ?? null,
4408
+ tokenAvailable: Boolean(profile?.token),
4409
+ tokenPrefix: profile?.tokenPrefix ?? null,
4410
+ tenantId: profile?.tenantId ?? null,
4411
+ artifactId: profile?.artifactId ?? null,
4412
+ permissions: profile?.permissions ?? [],
4413
+ expiresAt: formatExpiry(profile?.expiresAt),
4414
+ };
4415
+ if (command.json) {
4416
+ io.stdout(JSON.stringify(result, null, 2));
4417
+ }
4418
+ else if (profile) {
4419
+ io.stdout([
4420
+ "authenticated: true",
4421
+ `source: config:${command.profileName}`,
4422
+ `base_url: ${profile.baseUrl}`,
4423
+ `token_prefix: ${profile.tokenPrefix ?? "(unknown)"}`,
4424
+ `tenant_id: ${profile.tenantId ?? "(unknown)"}`,
4425
+ `app_id: ${profile.artifactId ?? "(account)"}`,
4426
+ `permissions: ${(profile.permissions ?? []).join(" ") || "(unknown)"}`,
4427
+ `expires_at: ${formatExpiry(profile.expiresAt) ?? "(unknown)"}`,
4428
+ ].join("\n"));
4429
+ }
4430
+ else {
4431
+ io.stdout([
4432
+ "authenticated: false",
4433
+ `profile: ${command.profileName}`,
4434
+ "next: tender auth login --device --profile edit-preview --artifact <artifact-id>",
4435
+ ].join("\n"));
4436
+ }
4437
+ return profile ? 0 : 1;
4438
+ }
3778
4439
  if (env.TENDER_API_TOKEN) {
3779
4440
  const result = {
3780
4441
  ok: true,
@@ -5491,14 +6152,11 @@ async function runAnalyticsQuery(command, io, runtime) {
5491
6152
  specPath: command.specPath,
5492
6153
  stdin: runtime.stdin,
5493
6154
  });
5494
- const payload = await requestAnalyticsApi({
5495
- artifactId: command.artifactId,
5496
- analyticsPath: "query",
5497
- method: "POST",
5498
- baseUrl: credentials.baseUrl,
5499
- token: credentials.token,
5500
- fetcher: runtime.fetcher ?? fetch,
5501
- body: { spec },
6155
+ const payload = await requestAnalyticsQueryWithPropertyGuidance({
6156
+ command,
6157
+ credentials,
6158
+ runtime,
6159
+ spec,
5502
6160
  });
5503
6161
  const rows = Array.isArray(payload.rows) ? payload.rows : [];
5504
6162
  printAnalyticsPayload(command, io, payload, [
@@ -5619,14 +6277,11 @@ async function runAnalyticsExportQuery(command, io, runtime) {
5619
6277
  specPath: command.specPath,
5620
6278
  stdin: runtime.stdin,
5621
6279
  });
5622
- const payload = await requestAnalyticsApi({
5623
- artifactId: command.artifactId,
5624
- analyticsPath: "query",
5625
- method: "POST",
5626
- baseUrl: credentials.baseUrl,
5627
- token: credentials.token,
5628
- fetcher: runtime.fetcher ?? fetch,
5629
- body: { spec },
6280
+ const payload = await requestAnalyticsQueryWithPropertyGuidance({
6281
+ command,
6282
+ credentials,
6283
+ runtime,
6284
+ spec,
5630
6285
  });
5631
6286
  const rows = Array.isArray(payload.rows) ? payload.rows : [];
5632
6287
  if (command.format === "csv") {
@@ -5636,6 +6291,58 @@ async function runAnalyticsExportQuery(command, io, runtime) {
5636
6291
  io.stdout(JSON.stringify(payload, null, 2));
5637
6292
  return 0;
5638
6293
  }
6294
+ async function runAnalyticsPropertiesList(command, io, runtime) {
6295
+ const credentials = await resolveAnalyticsCredentials(command, runtime);
6296
+ const payload = await requestAnalyticsApi({
6297
+ artifactId: command.artifactId,
6298
+ analyticsPath: "properties",
6299
+ method: "GET",
6300
+ baseUrl: credentials.baseUrl,
6301
+ token: credentials.token,
6302
+ fetcher: runtime.fetcher ?? fetch,
6303
+ });
6304
+ const properties = readAnalyticsPropertyRecords(payload).filter((property) => !command.eventName ||
6305
+ readCliStringField(property, "eventName") === command.eventName);
6306
+ printAnalyticsPayload(command, io, {
6307
+ ok: true,
6308
+ command: command.command,
6309
+ artifactId: command.artifactId,
6310
+ eventName: command.eventName,
6311
+ properties,
6312
+ }, [
6313
+ `app_id: ${command.artifactId}`,
6314
+ command.eventName ? `event: ${command.eventName}` : "event: all",
6315
+ `properties: ${properties.length}`,
6316
+ ]);
6317
+ return 0;
6318
+ }
6319
+ async function runAnalyticsPropertiesActivate(command, io, runtime) {
6320
+ const credentials = await resolveAnalyticsCredentials(command, runtime);
6321
+ const request = {
6322
+ eventName: command.eventName,
6323
+ propertyKey: command.propertyKey,
6324
+ propertyType: command.propertyType,
6325
+ label: command.label ?? command.propertyKey,
6326
+ dryRun: command.dryRun,
6327
+ };
6328
+ const payload = await requestAnalyticsApi({
6329
+ artifactId: command.artifactId,
6330
+ analyticsPath: "properties",
6331
+ method: "POST",
6332
+ baseUrl: credentials.baseUrl,
6333
+ token: credentials.token,
6334
+ fetcher: runtime.fetcher ?? fetch,
6335
+ body: request,
6336
+ });
6337
+ printAnalyticsPayload(command, io, payload, [
6338
+ `app_id: ${command.artifactId}`,
6339
+ `event: ${command.eventName}`,
6340
+ `property: ${command.propertyKey}`,
6341
+ `type: ${command.propertyType}`,
6342
+ ...(command.dryRun ? ["dry_run: true"] : []),
6343
+ ]);
6344
+ return 0;
6345
+ }
5639
6346
  async function runAnalyticsChartsCreate(command, io, runtime) {
5640
6347
  const spec = await readAnalyticsSpec({
5641
6348
  specPath: command.specPath,
@@ -5647,11 +6354,25 @@ async function runAnalyticsChartsCreate(command, io, runtime) {
5647
6354
  spec,
5648
6355
  layout: {},
5649
6356
  };
6357
+ const requiredProperties = readRequiredAnalyticsPropertiesFromSpec(spec);
6358
+ const credentials = !command.dryRun || requiredProperties.length > 0
6359
+ ? await resolveAnalyticsCredentials(command, runtime)
6360
+ : null;
6361
+ const properties = credentials
6362
+ ? await ensureAnalyticsProperties({
6363
+ command,
6364
+ credentials,
6365
+ runtime,
6366
+ requirements: requiredProperties,
6367
+ dryRun: command.dryRun,
6368
+ })
6369
+ : [];
5650
6370
  if (command.dryRun) {
5651
6371
  const payload = {
5652
6372
  ok: true,
5653
6373
  dryRun: true,
5654
6374
  artifactId: command.artifactId,
6375
+ properties,
5655
6376
  request: body,
5656
6377
  };
5657
6378
  printAnalyticsPayload(command, io, payload, [
@@ -5662,7 +6383,9 @@ async function runAnalyticsChartsCreate(command, io, runtime) {
5662
6383
  ]);
5663
6384
  return 0;
5664
6385
  }
5665
- const credentials = await resolveAnalyticsCredentials(command, runtime);
6386
+ if (!credentials) {
6387
+ throw new Error("Analytics credentials are required to create saved charts.");
6388
+ }
5666
6389
  const payload = await requestAnalyticsApi({
5667
6390
  artifactId: command.artifactId,
5668
6391
  analyticsPath: "charts",
@@ -5672,9 +6395,138 @@ async function runAnalyticsChartsCreate(command, io, runtime) {
5672
6395
  fetcher: runtime.fetcher ?? fetch,
5673
6396
  body,
5674
6397
  });
5675
- printAnalyticsPayload(command, io, payload, [
6398
+ printAnalyticsPayload(command, io, { ...payload, properties }, [
5676
6399
  `app_id: ${command.artifactId}`,
5677
6400
  `chart_id: ${String(payload.chart?.id ?? "")}`,
6401
+ `properties: ${properties.length}`,
6402
+ ]);
6403
+ return 0;
6404
+ }
6405
+ async function runAnalyticsChartsList(command, io, runtime) {
6406
+ const { payload } = await requestAnalyticsDashboardsForCli(command, runtime);
6407
+ const dashboard = findAnalyticsDashboard(payload, command.dashboardId);
6408
+ if (!dashboard) {
6409
+ throw new Error(`Analytics dashboard ${command.dashboardId} was not found for app ${command.artifactId}.`);
6410
+ }
6411
+ const charts = readAnalyticsChartRecords(dashboard);
6412
+ const dashboardName = readCliStringField(dashboard, "name");
6413
+ printAnalyticsPayload(command, io, {
6414
+ ok: true,
6415
+ command: command.command,
6416
+ artifactId: command.artifactId,
6417
+ dashboardId: command.dashboardId,
6418
+ dashboard: {
6419
+ id: command.dashboardId,
6420
+ name: dashboardName,
6421
+ },
6422
+ charts,
6423
+ }, [
6424
+ `app_id: ${command.artifactId}`,
6425
+ `dashboard_id: ${command.dashboardId}`,
6426
+ `charts: ${charts.length}`,
6427
+ ]);
6428
+ return 0;
6429
+ }
6430
+ async function runAnalyticsChartsUpdate(command, io, runtime) {
6431
+ const { credentials, payload: dashboardsPayload } = await requestAnalyticsDashboardsForCli(command, runtime);
6432
+ const existing = findAnalyticsChart(dashboardsPayload, command.chartId);
6433
+ if (!existing) {
6434
+ throw new Error(`Analytics chart ${command.chartId} was not found for app ${command.artifactId}.`);
6435
+ }
6436
+ const spec = command.specPath
6437
+ ? await readAnalyticsSpec({
6438
+ specPath: command.specPath,
6439
+ stdin: runtime.stdin,
6440
+ })
6441
+ : readAnalyticsChartSpec(existing.chart);
6442
+ const title = command.title ?? readAnalyticsChartTitle(existing.chart);
6443
+ const body = {
6444
+ title,
6445
+ spec,
6446
+ layout: readAnalyticsChartLayout(existing.chart),
6447
+ };
6448
+ const properties = await ensureAnalyticsProperties({
6449
+ command,
6450
+ credentials,
6451
+ runtime,
6452
+ requirements: readRequiredAnalyticsPropertiesFromSpec(spec),
6453
+ dryRun: command.dryRun,
6454
+ });
6455
+ const dashboardId = readCliStringField(existing.dashboard, "id");
6456
+ if (command.dryRun) {
6457
+ printAnalyticsPayload(command, io, {
6458
+ ok: true,
6459
+ dryRun: true,
6460
+ artifactId: command.artifactId,
6461
+ chartId: command.chartId,
6462
+ dashboardId,
6463
+ currentChart: existing.chart,
6464
+ properties,
6465
+ request: body,
6466
+ }, [
6467
+ `app_id: ${command.artifactId}`,
6468
+ `chart_id: ${command.chartId}`,
6469
+ `title: ${title}`,
6470
+ "dry_run: true",
6471
+ ]);
6472
+ return 0;
6473
+ }
6474
+ const payload = await requestAnalyticsApi({
6475
+ artifactId: command.artifactId,
6476
+ analyticsPath: "charts",
6477
+ resourceId: command.chartId,
6478
+ method: "PATCH",
6479
+ baseUrl: credentials.baseUrl,
6480
+ token: credentials.token,
6481
+ fetcher: runtime.fetcher ?? fetch,
6482
+ body,
6483
+ });
6484
+ printAnalyticsPayload(command, io, { ...payload, properties }, [
6485
+ `app_id: ${command.artifactId}`,
6486
+ `chart_id: ${command.chartId}`,
6487
+ `title: ${String(payload.chart?.title ?? title)}`,
6488
+ `properties: ${properties.length}`,
6489
+ ]);
6490
+ return 0;
6491
+ }
6492
+ async function runAnalyticsChartsDelete(command, io, runtime) {
6493
+ const { credentials, payload: dashboardsPayload } = await requestAnalyticsDashboardsForCli(command, runtime);
6494
+ const existing = findAnalyticsChart(dashboardsPayload, command.chartId);
6495
+ if (!existing) {
6496
+ throw new Error(`Analytics chart ${command.chartId} was not found for app ${command.artifactId}.`);
6497
+ }
6498
+ const dashboardId = readCliStringField(existing.dashboard, "id");
6499
+ if (command.dryRun) {
6500
+ printAnalyticsPayload(command, io, {
6501
+ ok: true,
6502
+ dryRun: true,
6503
+ artifactId: command.artifactId,
6504
+ chartId: command.chartId,
6505
+ dashboardId,
6506
+ currentChart: existing.chart,
6507
+ request: {
6508
+ chartId: command.chartId,
6509
+ },
6510
+ }, [
6511
+ `app_id: ${command.artifactId}`,
6512
+ `chart_id: ${command.chartId}`,
6513
+ "dry_run: true",
6514
+ ]);
6515
+ return 0;
6516
+ }
6517
+ const payload = await requestAnalyticsApi({
6518
+ artifactId: command.artifactId,
6519
+ analyticsPath: "charts",
6520
+ resourceId: command.chartId,
6521
+ method: "DELETE",
6522
+ baseUrl: credentials.baseUrl,
6523
+ token: credentials.token,
6524
+ fetcher: runtime.fetcher ?? fetch,
6525
+ });
6526
+ printAnalyticsPayload(command, io, payload, [
6527
+ `app_id: ${command.artifactId}`,
6528
+ `chart_id: ${command.chartId}`,
6529
+ `deleted: ${String(payload.deleted ?? "")}`,
5678
6530
  ]);
5679
6531
  return 0;
5680
6532
  }
@@ -5793,6 +6645,21 @@ export async function runTenderCli(args, io = {
5793
6645
  else if (command.topic === "app analytics charts create") {
5794
6646
  io.stdout(analyticsChartsCreateHelp());
5795
6647
  }
6648
+ else if (command.topic === "app analytics charts list") {
6649
+ io.stdout(analyticsChartsListHelp());
6650
+ }
6651
+ else if (command.topic === "app analytics charts update") {
6652
+ io.stdout(analyticsChartsUpdateHelp());
6653
+ }
6654
+ else if (command.topic === "app analytics charts delete") {
6655
+ io.stdout(analyticsChartsDeleteHelp());
6656
+ }
6657
+ else if (command.topic === "app analytics properties list") {
6658
+ io.stdout(analyticsPropertiesListHelp());
6659
+ }
6660
+ else if (command.topic === "app analytics properties activate") {
6661
+ io.stdout(analyticsPropertiesActivateHelp());
6662
+ }
5796
6663
  else if (command.topic === "app") {
5797
6664
  io.stdout(appHelp());
5798
6665
  }
@@ -5894,6 +6761,21 @@ export async function runTenderCli(args, io = {
5894
6761
  if (command.command === "app analytics charts create") {
5895
6762
  return await runAnalyticsChartsCreate(command, io, runtime);
5896
6763
  }
6764
+ if (command.command === "app analytics charts list") {
6765
+ return await runAnalyticsChartsList(command, io, runtime);
6766
+ }
6767
+ if (command.command === "app analytics charts update") {
6768
+ return await runAnalyticsChartsUpdate(command, io, runtime);
6769
+ }
6770
+ if (command.command === "app analytics charts delete") {
6771
+ return await runAnalyticsChartsDelete(command, io, runtime);
6772
+ }
6773
+ if (command.command === "app analytics properties list") {
6774
+ return await runAnalyticsPropertiesList(command, io, runtime);
6775
+ }
6776
+ if (command.command === "app analytics properties activate") {
6777
+ return await runAnalyticsPropertiesActivate(command, io, runtime);
6778
+ }
5897
6779
  if (command.command === "app git remote") {
5898
6780
  return await runGitRemote(command, io, runtime);
5899
6781
  }