@tenderprompt/cli 0.1.10 → 0.1.11

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 +46 -2
  2. package/dist/index.js +1060 -40
  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 property-slots list <app-id>
217
+ List promoted property slots
218
+ tender app analytics property-slots activate <app-id>
219
+ Activate a promoted property slot
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 property-slots list artifact_123 --json
259
+ tender app analytics property-slots 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 property-slots list <app-id> List promoted property slots
298
+ tender app analytics property-slots activate <app-id> Activate a promoted property slot
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 property-slots list artifact_123 --json
315
+ tender app analytics property-slots 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 activate required promoted property slots 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 activate required promoted property slots 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 analyticsPropertySlotsListHelp() {
425
+ return `Usage: tender app analytics property-slots list <app-id> [--event <event-name>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
426
+
427
+ Examples:
428
+ tender app analytics property-slots list artifact_123 --json
429
+ tender app analytics property-slots list artifact_123 --event signup_started --json`;
430
+ }
431
+ function analyticsPropertySlotsActivateHelp() {
432
+ return `Usage: tender app analytics property-slots activate <app-id> --event <event-name> --property <property-key> --type <dimension|metric> [--slot <slot-name>] [--label <label>] [--dry-run] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
433
+
434
+ Examples:
435
+ tender app analytics property-slots activate artifact_123 --event signup_started --property plan --type dimension --dry-run --json
436
+ tender app analytics property-slots 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 property-slots list <artifact-id> --json",
820
+ "tender app analytics property-slots 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 activate missing promoted property slots when the spec has an event_name = filter.",
831
+ "Ad hoc query is read-only; if it reports a missing property slot, activate the slot 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 property-slots list <artifact-id> --json",
890
+ purpose: "List active promoted property slots that make custom event properties queryable.",
891
+ requiresAuth: true,
892
+ writes: false,
893
+ },
894
+ {
895
+ command: "tender app analytics property-slots activate <artifact-id> --event <event-name> --property <property-key> --type <dimension|metric> --dry-run --json",
896
+ purpose: "Preview or activate the promoted slot 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>.");
@@ -2953,6 +3090,215 @@ 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 parseAnalyticsPropertySlotsListArgs(args) {
3210
+ if (args.includes("--help") || args.includes("-h")) {
3211
+ return { command: "help", topic: "app analytics property-slots list" };
3212
+ }
3213
+ const artifactId = args[0];
3214
+ if (!artifactId || artifactId.startsWith("-")) {
3215
+ throw new TenderCliUsageError("app id is required. Example: tender app analytics property-slots 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 property-slots list option: ${arg}`);
3227
+ }
3228
+ return {
3229
+ command: "app analytics property-slots list",
3230
+ artifactId,
3231
+ eventName,
3232
+ ...parsed,
3233
+ };
3234
+ }
3235
+ function parseAnalyticsPropertySlotsActivateArgs(args) {
3236
+ if (args.includes("--help") || args.includes("-h")) {
3237
+ return { command: "help", topic: "app analytics property-slots activate" };
3238
+ }
3239
+ const artifactId = args[0];
3240
+ if (!artifactId || artifactId.startsWith("-")) {
3241
+ throw new TenderCliUsageError("app id is required. Example: tender app analytics property-slots 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 slotType = null;
3247
+ let slotName = null;
3248
+ let label = null;
3249
+ let dryRun = false;
3250
+ for (let index = 0; index < parsed.remaining.length; index += 1) {
3251
+ const arg = parsed.remaining[index];
3252
+ if (arg === "--event") {
3253
+ eventName = parseRequiredFlagValue(parsed.remaining[index + 1], "--event");
3254
+ index += 1;
3255
+ continue;
3256
+ }
3257
+ if (arg === "--property") {
3258
+ propertyKey = parseRequiredFlagValue(parsed.remaining[index + 1], "--property");
3259
+ index += 1;
3260
+ continue;
3261
+ }
3262
+ if (arg === "--type") {
3263
+ const value = parseRequiredFlagValue(parsed.remaining[index + 1], "--type");
3264
+ if (value !== "dimension" && value !== "metric") {
3265
+ throw new TenderCliUsageError("--type must be dimension or metric.");
3266
+ }
3267
+ slotType = value;
3268
+ index += 1;
3269
+ continue;
3270
+ }
3271
+ if (arg === "--slot") {
3272
+ slotName = parseRequiredFlagValue(parsed.remaining[index + 1], "--slot");
3273
+ index += 1;
3274
+ continue;
3275
+ }
3276
+ if (arg === "--label") {
3277
+ label = parseRequiredFlagValue(parsed.remaining[index + 1], "--label");
3278
+ index += 1;
3279
+ continue;
3280
+ }
3281
+ if (arg === "--dry-run") {
3282
+ dryRun = true;
3283
+ continue;
3284
+ }
3285
+ throw new TenderCliUsageError(`Unknown analytics property-slots activate option: ${arg}`);
3286
+ }
3287
+ if (!eventName || !propertyKey || !slotType) {
3288
+ throw new TenderCliUsageError("--event, --property, and --type are required. Example: tender app analytics property-slots activate artifact_123 --event signup_started --property plan --type dimension --json");
3289
+ }
3290
+ return {
3291
+ command: "app analytics property-slots activate",
3292
+ artifactId,
3293
+ eventName,
3294
+ propertyKey,
3295
+ slotType,
3296
+ slotName,
3297
+ label,
3298
+ dryRun,
3299
+ ...parsed,
3300
+ };
3301
+ }
2956
3302
  function parseTenderArgs(args) {
2957
3303
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
2958
3304
  return { command: "help", topic: "root" };
@@ -3036,6 +3382,23 @@ function parseTenderArgs(args) {
3036
3382
  if (resourceArgs[1] === "analytics" && resourceArgs[2] === "dashboards") {
3037
3383
  return parseAnalyticsDashboardsArgs(resourceArgs.slice(3));
3038
3384
  }
3385
+ if (resourceArgs[1] === "analytics" &&
3386
+ resourceArgs[2] === "property-slots" &&
3387
+ (resourceArgs[3] === undefined ||
3388
+ resourceArgs[3] === "--help" ||
3389
+ resourceArgs[3] === "-h")) {
3390
+ return { command: "help", topic: "app analytics property-slots list" };
3391
+ }
3392
+ if (resourceArgs[1] === "analytics" &&
3393
+ resourceArgs[2] === "property-slots" &&
3394
+ resourceArgs[3] === "list") {
3395
+ return parseAnalyticsPropertySlotsListArgs(resourceArgs.slice(4));
3396
+ }
3397
+ if (resourceArgs[1] === "analytics" &&
3398
+ resourceArgs[2] === "property-slots" &&
3399
+ resourceArgs[3] === "activate") {
3400
+ return parseAnalyticsPropertySlotsActivateArgs(resourceArgs.slice(4));
3401
+ }
3039
3402
  if (resourceArgs[1] === "analytics" &&
3040
3403
  resourceArgs[2] === "export" &&
3041
3404
  (resourceArgs[3] === undefined ||
@@ -3070,6 +3433,21 @@ function parseTenderArgs(args) {
3070
3433
  resourceArgs[3] === "create") {
3071
3434
  return parseAnalyticsChartsCreateArgs(resourceArgs.slice(4));
3072
3435
  }
3436
+ if (resourceArgs[1] === "analytics" &&
3437
+ resourceArgs[2] === "charts" &&
3438
+ resourceArgs[3] === "list") {
3439
+ return parseAnalyticsChartsListArgs(resourceArgs.slice(4));
3440
+ }
3441
+ if (resourceArgs[1] === "analytics" &&
3442
+ resourceArgs[2] === "charts" &&
3443
+ resourceArgs[3] === "update") {
3444
+ return parseAnalyticsChartsUpdateArgs(resourceArgs.slice(4));
3445
+ }
3446
+ if (resourceArgs[1] === "analytics" &&
3447
+ resourceArgs[2] === "charts" &&
3448
+ resourceArgs[3] === "delete") {
3449
+ return parseAnalyticsChartsDeleteArgs(resourceArgs.slice(4));
3450
+ }
3073
3451
  if (resourceArgs[1] === "context" &&
3074
3452
  (resourceArgs[2] === undefined ||
3075
3453
  resourceArgs[2] === "--help" ||
@@ -3442,7 +3820,9 @@ async function requestAnalyticsApi(input) {
3442
3820
  if (input.includeCatalog) {
3443
3821
  params.set("includeCatalog", "1");
3444
3822
  }
3445
- const suffix = input.analyticsPath ? `/${input.analyticsPath}` : "";
3823
+ const suffix = input.analyticsPath
3824
+ ? `/${input.analyticsPath}${input.resourceId ? `/${encodeURIComponent(input.resourceId)}` : ""}`
3825
+ : "";
3446
3826
  const query = params.size > 0 ? `?${params.toString()}` : "";
3447
3827
  const response = await input.fetcher(`${input.baseUrl}/api/v1/artifacts/${encodeURIComponent(input.artifactId)}/analytics${suffix}${query}`, {
3448
3828
  method: input.method,
@@ -3460,12 +3840,12 @@ async function requestAnalyticsApi(input) {
3460
3840
  const message = (typeof payload?.message === "string" && payload.message) ||
3461
3841
  (typeof payload?.reasonCode === "string" && payload.reasonCode) ||
3462
3842
  `Analytics request failed with HTTP ${response.status}.`;
3463
- throw new Error([
3843
+ throw new TenderCliAnalyticsApiError([
3464
3844
  message,
3465
3845
  `Scope: artifact ${input.artifactId}, analytics/${input.analyticsPath || "summary"}.`,
3466
3846
  `Reason: ${reasonCode}.`,
3467
3847
  `Example: ${analyticsCorrectedExample(input.artifactId, input.analyticsPath)}.`,
3468
- ].join("\n"));
3848
+ ].join("\n"), reasonCode);
3469
3849
  }
3470
3850
  return payload;
3471
3851
  }
@@ -3479,6 +3859,9 @@ function analyticsCorrectedExample(artifactId, analyticsPath) {
3479
3859
  if (analyticsPath === "suggestions") {
3480
3860
  return `tender app analytics suggestions ${artifactId} --range 30d --json`;
3481
3861
  }
3862
+ if (analyticsPath === "property-slots") {
3863
+ return `tender app analytics property-slots list ${artifactId} --json`;
3864
+ }
3482
3865
  if (analyticsPath === "dashboards") {
3483
3866
  return `tender app analytics dashboards ${artifactId} --json`;
3484
3867
  }
@@ -3487,6 +3870,332 @@ function analyticsCorrectedExample(artifactId, analyticsPath) {
3487
3870
  }
3488
3871
  return `tender app analytics summary ${artifactId} --range 30d --json`;
3489
3872
  }
3873
+ function isCliJsonRecord(value) {
3874
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3875
+ }
3876
+ function readCliStringField(record, field) {
3877
+ const value = record[field];
3878
+ return typeof value === "string" ? value : null;
3879
+ }
3880
+ function readAnalyticsDashboardRecords(payload) {
3881
+ return Array.isArray(payload.dashboards)
3882
+ ? payload.dashboards.filter(isCliJsonRecord)
3883
+ : [];
3884
+ }
3885
+ function readAnalyticsChartRecords(dashboard) {
3886
+ return Array.isArray(dashboard.charts)
3887
+ ? dashboard.charts.filter(isCliJsonRecord)
3888
+ : [];
3889
+ }
3890
+ function findAnalyticsDashboard(payload, dashboardId) {
3891
+ return (readAnalyticsDashboardRecords(payload).find((dashboard) => readCliStringField(dashboard, "id") === dashboardId) ?? null);
3892
+ }
3893
+ function findAnalyticsChart(payload, chartId) {
3894
+ for (const dashboard of readAnalyticsDashboardRecords(payload)) {
3895
+ const chart = readAnalyticsChartRecords(dashboard).find((candidate) => readCliStringField(candidate, "id") === chartId);
3896
+ if (chart) {
3897
+ return { dashboard, chart };
3898
+ }
3899
+ }
3900
+ return null;
3901
+ }
3902
+ function readAnalyticsChartTitle(chart) {
3903
+ return readCliStringField(chart, "title") ?? "Analytics chart";
3904
+ }
3905
+ function readAnalyticsChartSpec(chart) {
3906
+ const spec = chart.spec;
3907
+ if (spec === undefined) {
3908
+ throw new Error("Saved chart does not include a chart spec.");
3909
+ }
3910
+ return spec;
3911
+ }
3912
+ function readAnalyticsChartLayout(chart) {
3913
+ return isCliJsonRecord(chart.layout) ? chart.layout : {};
3914
+ }
3915
+ function readAnalyticsPropertySlotRecords(payload) {
3916
+ return Array.isArray(payload.propertySlots)
3917
+ ? payload.propertySlots.filter(isCliJsonRecord)
3918
+ : [];
3919
+ }
3920
+ function readAnalyticsSpecEventNameFilter(spec) {
3921
+ if (!isCliJsonRecord(spec) || !Array.isArray(spec.filters)) {
3922
+ return null;
3923
+ }
3924
+ const filter = spec.filters.filter(isCliJsonRecord).find((candidate) => readCliStringField(candidate, "field") === "event_name" &&
3925
+ readCliStringField(candidate, "op") === "=" &&
3926
+ typeof candidate.value === "string" &&
3927
+ candidate.value.trim().length > 0);
3928
+ return typeof filter?.value === "string" ? filter.value.trim() : null;
3929
+ }
3930
+ function readAnalyticsSpecMetricName(spec) {
3931
+ if (!isCliJsonRecord(spec) || !isCliJsonRecord(spec.metric)) {
3932
+ return null;
3933
+ }
3934
+ return readCliStringField(spec.metric, "name");
3935
+ }
3936
+ function readAnalyticsSpecGroupField(spec) {
3937
+ if (!isCliJsonRecord(spec) || !Array.isArray(spec.groupBy)) {
3938
+ return null;
3939
+ }
3940
+ const [group] = spec.groupBy.filter(isCliJsonRecord);
3941
+ return group ? readCliStringField(group, "field") : null;
3942
+ }
3943
+ function readRequiredAnalyticsPropertySlotsFromSpec(spec) {
3944
+ const eventName = readAnalyticsSpecEventNameFilter(spec);
3945
+ const requirements = [];
3946
+ const groupField = readAnalyticsSpecGroupField(spec);
3947
+ if (groupField?.startsWith("property:")) {
3948
+ if (!eventName) {
3949
+ throw new TenderCliUsageError("Property group-by chart specs require an event_name = filter so the CLI can activate the promoted property slot.");
3950
+ }
3951
+ requirements.push({
3952
+ eventName,
3953
+ propertyKey: groupField.slice("property:".length),
3954
+ slotType: "dimension",
3955
+ });
3956
+ }
3957
+ const metricName = readAnalyticsSpecMetricName(spec);
3958
+ if (metricName?.startsWith("sum:") || metricName?.startsWith("avg:")) {
3959
+ if (!eventName) {
3960
+ throw new TenderCliUsageError("Property metric chart specs require an event_name = filter so the CLI can activate the promoted property slot.");
3961
+ }
3962
+ requirements.push({
3963
+ eventName,
3964
+ propertyKey: metricName.slice(metricName.indexOf(":") + 1),
3965
+ slotType: "metric",
3966
+ });
3967
+ }
3968
+ const seen = new Set();
3969
+ return requirements.filter((requirement) => {
3970
+ const key = `${requirement.slotType}:${requirement.eventName}:${requirement.propertyKey}`;
3971
+ if (seen.has(key)) {
3972
+ return false;
3973
+ }
3974
+ seen.add(key);
3975
+ return true;
3976
+ });
3977
+ }
3978
+ function analyticsPropertySlotNames(slotType) {
3979
+ return slotType === "dimension"
3980
+ ? Array.from({ length: 13 }, (_, index) => `blob${index + 8}`)
3981
+ : Array.from({ length: 19 }, (_, index) => `double${index + 2}`);
3982
+ }
3983
+ function isValidAnalyticsPropertySlotName(input) {
3984
+ return analyticsPropertySlotNames(input.slotType).includes(input.slotName);
3985
+ }
3986
+ function findExistingAnalyticsPropertySlot(input) {
3987
+ return (input.propertySlots.find((slot) => readCliStringField(slot, "status") === "active" &&
3988
+ readCliStringField(slot, "eventName") === input.requirement.eventName &&
3989
+ readCliStringField(slot, "propertyKey") === input.requirement.propertyKey &&
3990
+ readCliStringField(slot, "slotType") === input.requirement.slotType) ?? null);
3991
+ }
3992
+ function chooseAnalyticsPropertySlotName(input) {
3993
+ if (input.requestedSlotName) {
3994
+ if (!isValidAnalyticsPropertySlotName({
3995
+ slotType: input.requirement.slotType,
3996
+ slotName: input.requestedSlotName,
3997
+ })) {
3998
+ throw new TenderCliUsageError(`--slot must be a valid ${input.requirement.slotType} analytics slot.`);
3999
+ }
4000
+ const conflictingSlot = input.propertySlots.find((slot) => readCliStringField(slot, "status") === "active" &&
4001
+ readCliStringField(slot, "eventName") === input.requirement.eventName &&
4002
+ readCliStringField(slot, "slotType") === input.requirement.slotType &&
4003
+ readCliStringField(slot, "slotName") === input.requestedSlotName &&
4004
+ readCliStringField(slot, "propertyKey") !== input.requirement.propertyKey);
4005
+ if (conflictingSlot) {
4006
+ throw new TenderCliUsageError(`--slot ${input.requestedSlotName} is already active for another ${input.requirement.slotType} property on ${input.requirement.eventName}.`);
4007
+ }
4008
+ return input.requestedSlotName;
4009
+ }
4010
+ const usedSlots = new Set(input.propertySlots
4011
+ .filter((slot) => readCliStringField(slot, "status") === "active" &&
4012
+ readCliStringField(slot, "eventName") === input.requirement.eventName &&
4013
+ readCliStringField(slot, "slotType") === input.requirement.slotType)
4014
+ .map((slot) => readCliStringField(slot, "slotName"))
4015
+ .filter((slotName) => Boolean(slotName)));
4016
+ const slotName = analyticsPropertySlotNames(input.requirement.slotType).find((candidate) => !usedSlots.has(candidate));
4017
+ if (!slotName) {
4018
+ throw new Error("analytics_property_slots_full");
4019
+ }
4020
+ return slotName;
4021
+ }
4022
+ async function fetchAnalyticsPropertySlots(input) {
4023
+ const payload = await requestAnalyticsApi({
4024
+ artifactId: input.command.artifactId,
4025
+ analyticsPath: "property-slots",
4026
+ method: "GET",
4027
+ baseUrl: input.credentials.baseUrl,
4028
+ token: input.credentials.token,
4029
+ fetcher: input.runtime.fetcher ?? fetch,
4030
+ });
4031
+ return { payload, propertySlots: readAnalyticsPropertySlotRecords(payload) };
4032
+ }
4033
+ async function ensureAnalyticsPropertySlots(input) {
4034
+ if (input.requirements.length === 0) {
4035
+ return [];
4036
+ }
4037
+ const { propertySlots } = await fetchAnalyticsPropertySlots({
4038
+ command: input.command,
4039
+ credentials: input.credentials,
4040
+ runtime: input.runtime,
4041
+ });
4042
+ const plan = [];
4043
+ for (const requirement of input.requirements) {
4044
+ const existing = findExistingAnalyticsPropertySlot({
4045
+ propertySlots,
4046
+ requirement,
4047
+ });
4048
+ if (existing) {
4049
+ plan.push({
4050
+ ...requirement,
4051
+ slotName: readCliStringField(existing, "slotName") ?? "",
4052
+ label: readCliStringField(existing, "label") ?? requirement.propertyKey,
4053
+ status: "existing",
4054
+ propertySlot: existing,
4055
+ });
4056
+ continue;
4057
+ }
4058
+ const slotName = chooseAnalyticsPropertySlotName({
4059
+ propertySlots,
4060
+ requirement,
4061
+ });
4062
+ const body = {
4063
+ eventName: requirement.eventName,
4064
+ propertyKey: requirement.propertyKey,
4065
+ slotType: requirement.slotType,
4066
+ slotName,
4067
+ label: requirement.propertyKey,
4068
+ };
4069
+ if (input.dryRun) {
4070
+ plan.push({
4071
+ ...body,
4072
+ status: "planned",
4073
+ });
4074
+ continue;
4075
+ }
4076
+ const payload = await requestAnalyticsApi({
4077
+ artifactId: input.command.artifactId,
4078
+ analyticsPath: "property-slots",
4079
+ method: "POST",
4080
+ baseUrl: input.credentials.baseUrl,
4081
+ token: input.credentials.token,
4082
+ fetcher: input.runtime.fetcher ?? fetch,
4083
+ body,
4084
+ });
4085
+ plan.push({
4086
+ ...body,
4087
+ status: "activated",
4088
+ propertySlot: payload.propertySlot,
4089
+ });
4090
+ propertySlots.push({
4091
+ eventName: body.eventName,
4092
+ propertyKey: body.propertyKey,
4093
+ slotType: body.slotType,
4094
+ slotName: body.slotName,
4095
+ label: body.label,
4096
+ status: "active",
4097
+ });
4098
+ }
4099
+ return plan;
4100
+ }
4101
+ function analyticsCommandAuthFlags(command) {
4102
+ const flags = [];
4103
+ if (command.baseUrl) {
4104
+ flags.push("--base-url", shellQuote(command.baseUrl));
4105
+ }
4106
+ if (command.profileName) {
4107
+ flags.push("--profile", shellQuote(command.profileName));
4108
+ }
4109
+ return flags;
4110
+ }
4111
+ function analyticsPropertySlotActivationCommand(input) {
4112
+ return [
4113
+ "tender",
4114
+ "app",
4115
+ "analytics",
4116
+ "property-slots",
4117
+ "activate",
4118
+ shellQuote(input.command.artifactId),
4119
+ "--event",
4120
+ shellQuote(input.requirement.eventName),
4121
+ "--property",
4122
+ shellQuote(input.requirement.propertyKey),
4123
+ "--type",
4124
+ input.requirement.slotType,
4125
+ ...(input.dryRun ? ["--dry-run"] : []),
4126
+ ...analyticsCommandAuthFlags(input.command),
4127
+ "--json",
4128
+ ].join(" ");
4129
+ }
4130
+ function createAnalyticsPropertySlotGuidance(input) {
4131
+ let requirements;
4132
+ try {
4133
+ requirements = readRequiredAnalyticsPropertySlotsFromSpec(input.spec);
4134
+ }
4135
+ catch {
4136
+ return null;
4137
+ }
4138
+ if (requirements.length === 0) {
4139
+ return null;
4140
+ }
4141
+ const lines = [
4142
+ "To make this chart available for dashboard queries, activate the custom event property first:",
4143
+ ];
4144
+ for (const requirement of requirements) {
4145
+ lines.push(analyticsPropertySlotActivationCommand({
4146
+ command: input.command,
4147
+ requirement,
4148
+ dryRun: true,
4149
+ }), analyticsPropertySlotActivationCommand({
4150
+ command: input.command,
4151
+ requirement,
4152
+ dryRun: false,
4153
+ }));
4154
+ }
4155
+ if (input.command.flagToken) {
4156
+ lines.push("Pass the same --token again, or use a saved profile/TENDER_API_TOKEN; tokens are not echoed in suggestions.");
4157
+ }
4158
+ lines.push("Then rerun the original analytics query.");
4159
+ return lines.join("\n");
4160
+ }
4161
+ async function requestAnalyticsQueryWithPropertyGuidance(input) {
4162
+ try {
4163
+ return await requestAnalyticsApi({
4164
+ artifactId: input.command.artifactId,
4165
+ analyticsPath: "query",
4166
+ method: "POST",
4167
+ baseUrl: input.credentials.baseUrl,
4168
+ token: input.credentials.token,
4169
+ fetcher: input.runtime.fetcher ?? fetch,
4170
+ body: { spec: input.spec },
4171
+ });
4172
+ }
4173
+ catch (error) {
4174
+ if (error instanceof TenderCliAnalyticsApiError &&
4175
+ error.reasonCode === "analytics_property_slot_not_found") {
4176
+ const guidance = createAnalyticsPropertySlotGuidance({
4177
+ command: input.command,
4178
+ spec: input.spec,
4179
+ });
4180
+ if (guidance) {
4181
+ throw new TenderCliAnalyticsApiError(`${error.message}\n\n${guidance}`, error.reasonCode);
4182
+ }
4183
+ }
4184
+ throw error;
4185
+ }
4186
+ }
4187
+ async function requestAnalyticsDashboardsForCli(command, runtime) {
4188
+ const credentials = await resolveAnalyticsCredentials(command, runtime);
4189
+ const payload = await requestAnalyticsApi({
4190
+ artifactId: command.artifactId,
4191
+ analyticsPath: "dashboards",
4192
+ method: "GET",
4193
+ baseUrl: credentials.baseUrl,
4194
+ token: credentials.token,
4195
+ fetcher: runtime.fetcher ?? fetch,
4196
+ });
4197
+ return { credentials, payload };
4198
+ }
3490
4199
  function printAnalyticsPayload(command, io, payload, fallbackLines) {
3491
4200
  if (command.json) {
3492
4201
  io.stdout(JSON.stringify(payload, null, 2));
@@ -3775,6 +4484,47 @@ async function planSourceFiles(input) {
3775
4484
  }
3776
4485
  async function runAuthStatus(command, io, runtime) {
3777
4486
  const env = runtime.env ?? process.env;
4487
+ if (command.profileName) {
4488
+ const config = await readConfig(env);
4489
+ const profile = config.profiles?.[command.profileName] ?? null;
4490
+ const result = {
4491
+ ok: Boolean(profile),
4492
+ command: command.command,
4493
+ source: profile ? "config" : null,
4494
+ profile: command.profileName,
4495
+ configPath: readConfigPath(env),
4496
+ baseUrl: profile?.baseUrl ?? null,
4497
+ tokenAvailable: Boolean(profile?.token),
4498
+ tokenPrefix: profile?.tokenPrefix ?? null,
4499
+ tenantId: profile?.tenantId ?? null,
4500
+ artifactId: profile?.artifactId ?? null,
4501
+ permissions: profile?.permissions ?? [],
4502
+ expiresAt: formatExpiry(profile?.expiresAt),
4503
+ };
4504
+ if (command.json) {
4505
+ io.stdout(JSON.stringify(result, null, 2));
4506
+ }
4507
+ else if (profile) {
4508
+ io.stdout([
4509
+ "authenticated: true",
4510
+ `source: config:${command.profileName}`,
4511
+ `base_url: ${profile.baseUrl}`,
4512
+ `token_prefix: ${profile.tokenPrefix ?? "(unknown)"}`,
4513
+ `tenant_id: ${profile.tenantId ?? "(unknown)"}`,
4514
+ `app_id: ${profile.artifactId ?? "(account)"}`,
4515
+ `permissions: ${(profile.permissions ?? []).join(" ") || "(unknown)"}`,
4516
+ `expires_at: ${formatExpiry(profile.expiresAt) ?? "(unknown)"}`,
4517
+ ].join("\n"));
4518
+ }
4519
+ else {
4520
+ io.stdout([
4521
+ "authenticated: false",
4522
+ `profile: ${command.profileName}`,
4523
+ "next: tender auth login --device --profile edit-preview --artifact <artifact-id>",
4524
+ ].join("\n"));
4525
+ }
4526
+ return profile ? 0 : 1;
4527
+ }
3778
4528
  if (env.TENDER_API_TOKEN) {
3779
4529
  const result = {
3780
4530
  ok: true,
@@ -5491,14 +6241,11 @@ async function runAnalyticsQuery(command, io, runtime) {
5491
6241
  specPath: command.specPath,
5492
6242
  stdin: runtime.stdin,
5493
6243
  });
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 },
6244
+ const payload = await requestAnalyticsQueryWithPropertyGuidance({
6245
+ command,
6246
+ credentials,
6247
+ runtime,
6248
+ spec,
5502
6249
  });
5503
6250
  const rows = Array.isArray(payload.rows) ? payload.rows : [];
5504
6251
  printAnalyticsPayload(command, io, payload, [
@@ -5619,21 +6366,119 @@ async function runAnalyticsExportQuery(command, io, runtime) {
5619
6366
  specPath: command.specPath,
5620
6367
  stdin: runtime.stdin,
5621
6368
  });
6369
+ const payload = await requestAnalyticsQueryWithPropertyGuidance({
6370
+ command,
6371
+ credentials,
6372
+ runtime,
6373
+ spec,
6374
+ });
6375
+ const rows = Array.isArray(payload.rows) ? payload.rows : [];
6376
+ if (command.format === "csv") {
6377
+ io.stdout(rowsToCsv(rows));
6378
+ return 0;
6379
+ }
6380
+ io.stdout(JSON.stringify(payload, null, 2));
6381
+ return 0;
6382
+ }
6383
+ async function runAnalyticsPropertySlotsList(command, io, runtime) {
6384
+ const credentials = await resolveAnalyticsCredentials(command, runtime);
5622
6385
  const payload = await requestAnalyticsApi({
5623
6386
  artifactId: command.artifactId,
5624
- analyticsPath: "query",
5625
- method: "POST",
6387
+ analyticsPath: "property-slots",
6388
+ method: "GET",
5626
6389
  baseUrl: credentials.baseUrl,
5627
6390
  token: credentials.token,
5628
6391
  fetcher: runtime.fetcher ?? fetch,
5629
- body: { spec },
5630
6392
  });
5631
- const rows = Array.isArray(payload.rows) ? payload.rows : [];
5632
- if (command.format === "csv") {
5633
- io.stdout(rowsToCsv(rows));
6393
+ const propertySlots = readAnalyticsPropertySlotRecords(payload).filter((slot) => !command.eventName ||
6394
+ readCliStringField(slot, "eventName") === command.eventName);
6395
+ printAnalyticsPayload(command, io, {
6396
+ ok: true,
6397
+ command: command.command,
6398
+ artifactId: command.artifactId,
6399
+ eventName: command.eventName,
6400
+ propertySlots,
6401
+ }, [
6402
+ `app_id: ${command.artifactId}`,
6403
+ command.eventName ? `event: ${command.eventName}` : "event: all",
6404
+ `property_slots: ${propertySlots.length}`,
6405
+ ]);
6406
+ return 0;
6407
+ }
6408
+ async function runAnalyticsPropertySlotsActivate(command, io, runtime) {
6409
+ const credentials = await resolveAnalyticsCredentials(command, runtime);
6410
+ const requirement = {
6411
+ eventName: command.eventName,
6412
+ propertyKey: command.propertyKey,
6413
+ slotType: command.slotType,
6414
+ };
6415
+ const { propertySlots } = await fetchAnalyticsPropertySlots({
6416
+ command,
6417
+ credentials,
6418
+ runtime,
6419
+ });
6420
+ const existing = findExistingAnalyticsPropertySlot({
6421
+ propertySlots,
6422
+ requirement,
6423
+ });
6424
+ if (existing) {
6425
+ const payload = {
6426
+ ok: true,
6427
+ command: command.command,
6428
+ artifactId: command.artifactId,
6429
+ alreadyActive: true,
6430
+ propertySlot: existing,
6431
+ };
6432
+ printAnalyticsPayload(command, io, payload, [
6433
+ `app_id: ${command.artifactId}`,
6434
+ "already_active: true",
6435
+ `slot_name: ${readCliStringField(existing, "slotName") ?? ""}`,
6436
+ ]);
5634
6437
  return 0;
5635
6438
  }
5636
- io.stdout(JSON.stringify(payload, null, 2));
6439
+ const slotName = chooseAnalyticsPropertySlotName({
6440
+ propertySlots,
6441
+ requirement,
6442
+ requestedSlotName: command.slotName,
6443
+ });
6444
+ const request = {
6445
+ eventName: command.eventName,
6446
+ propertyKey: command.propertyKey,
6447
+ slotType: command.slotType,
6448
+ slotName,
6449
+ label: command.label ?? command.propertyKey,
6450
+ };
6451
+ if (command.dryRun) {
6452
+ printAnalyticsPayload(command, io, {
6453
+ ok: true,
6454
+ command: command.command,
6455
+ dryRun: true,
6456
+ artifactId: command.artifactId,
6457
+ request,
6458
+ }, [
6459
+ `app_id: ${command.artifactId}`,
6460
+ `event: ${command.eventName}`,
6461
+ `property: ${command.propertyKey}`,
6462
+ `slot_name: ${slotName}`,
6463
+ "dry_run: true",
6464
+ ]);
6465
+ return 0;
6466
+ }
6467
+ const payload = await requestAnalyticsApi({
6468
+ artifactId: command.artifactId,
6469
+ analyticsPath: "property-slots",
6470
+ method: "POST",
6471
+ baseUrl: credentials.baseUrl,
6472
+ token: credentials.token,
6473
+ fetcher: runtime.fetcher ?? fetch,
6474
+ body: request,
6475
+ });
6476
+ printAnalyticsPayload(command, io, payload, [
6477
+ `app_id: ${command.artifactId}`,
6478
+ `event: ${command.eventName}`,
6479
+ `property: ${command.propertyKey}`,
6480
+ `slot_name: ${slotName}`,
6481
+ ]);
5637
6482
  return 0;
5638
6483
  }
5639
6484
  async function runAnalyticsChartsCreate(command, io, runtime) {
@@ -5647,11 +6492,25 @@ async function runAnalyticsChartsCreate(command, io, runtime) {
5647
6492
  spec,
5648
6493
  layout: {},
5649
6494
  };
6495
+ const requiredPropertySlots = readRequiredAnalyticsPropertySlotsFromSpec(spec);
6496
+ const credentials = !command.dryRun || requiredPropertySlots.length > 0
6497
+ ? await resolveAnalyticsCredentials(command, runtime)
6498
+ : null;
6499
+ const propertySlots = credentials
6500
+ ? await ensureAnalyticsPropertySlots({
6501
+ command,
6502
+ credentials,
6503
+ runtime,
6504
+ requirements: requiredPropertySlots,
6505
+ dryRun: command.dryRun,
6506
+ })
6507
+ : [];
5650
6508
  if (command.dryRun) {
5651
6509
  const payload = {
5652
6510
  ok: true,
5653
6511
  dryRun: true,
5654
6512
  artifactId: command.artifactId,
6513
+ propertySlots,
5655
6514
  request: body,
5656
6515
  };
5657
6516
  printAnalyticsPayload(command, io, payload, [
@@ -5662,7 +6521,9 @@ async function runAnalyticsChartsCreate(command, io, runtime) {
5662
6521
  ]);
5663
6522
  return 0;
5664
6523
  }
5665
- const credentials = await resolveAnalyticsCredentials(command, runtime);
6524
+ if (!credentials) {
6525
+ throw new Error("Analytics credentials are required to create saved charts.");
6526
+ }
5666
6527
  const payload = await requestAnalyticsApi({
5667
6528
  artifactId: command.artifactId,
5668
6529
  analyticsPath: "charts",
@@ -5672,9 +6533,138 @@ async function runAnalyticsChartsCreate(command, io, runtime) {
5672
6533
  fetcher: runtime.fetcher ?? fetch,
5673
6534
  body,
5674
6535
  });
5675
- printAnalyticsPayload(command, io, payload, [
6536
+ printAnalyticsPayload(command, io, { ...payload, propertySlots }, [
5676
6537
  `app_id: ${command.artifactId}`,
5677
6538
  `chart_id: ${String(payload.chart?.id ?? "")}`,
6539
+ `property_slots: ${propertySlots.length}`,
6540
+ ]);
6541
+ return 0;
6542
+ }
6543
+ async function runAnalyticsChartsList(command, io, runtime) {
6544
+ const { payload } = await requestAnalyticsDashboardsForCli(command, runtime);
6545
+ const dashboard = findAnalyticsDashboard(payload, command.dashboardId);
6546
+ if (!dashboard) {
6547
+ throw new Error(`Analytics dashboard ${command.dashboardId} was not found for app ${command.artifactId}.`);
6548
+ }
6549
+ const charts = readAnalyticsChartRecords(dashboard);
6550
+ const dashboardName = readCliStringField(dashboard, "name");
6551
+ printAnalyticsPayload(command, io, {
6552
+ ok: true,
6553
+ command: command.command,
6554
+ artifactId: command.artifactId,
6555
+ dashboardId: command.dashboardId,
6556
+ dashboard: {
6557
+ id: command.dashboardId,
6558
+ name: dashboardName,
6559
+ },
6560
+ charts,
6561
+ }, [
6562
+ `app_id: ${command.artifactId}`,
6563
+ `dashboard_id: ${command.dashboardId}`,
6564
+ `charts: ${charts.length}`,
6565
+ ]);
6566
+ return 0;
6567
+ }
6568
+ async function runAnalyticsChartsUpdate(command, io, runtime) {
6569
+ const { credentials, payload: dashboardsPayload } = await requestAnalyticsDashboardsForCli(command, runtime);
6570
+ const existing = findAnalyticsChart(dashboardsPayload, command.chartId);
6571
+ if (!existing) {
6572
+ throw new Error(`Analytics chart ${command.chartId} was not found for app ${command.artifactId}.`);
6573
+ }
6574
+ const spec = command.specPath
6575
+ ? await readAnalyticsSpec({
6576
+ specPath: command.specPath,
6577
+ stdin: runtime.stdin,
6578
+ })
6579
+ : readAnalyticsChartSpec(existing.chart);
6580
+ const title = command.title ?? readAnalyticsChartTitle(existing.chart);
6581
+ const body = {
6582
+ title,
6583
+ spec,
6584
+ layout: readAnalyticsChartLayout(existing.chart),
6585
+ };
6586
+ const propertySlots = await ensureAnalyticsPropertySlots({
6587
+ command,
6588
+ credentials,
6589
+ runtime,
6590
+ requirements: readRequiredAnalyticsPropertySlotsFromSpec(spec),
6591
+ dryRun: command.dryRun,
6592
+ });
6593
+ const dashboardId = readCliStringField(existing.dashboard, "id");
6594
+ if (command.dryRun) {
6595
+ printAnalyticsPayload(command, io, {
6596
+ ok: true,
6597
+ dryRun: true,
6598
+ artifactId: command.artifactId,
6599
+ chartId: command.chartId,
6600
+ dashboardId,
6601
+ currentChart: existing.chart,
6602
+ propertySlots,
6603
+ request: body,
6604
+ }, [
6605
+ `app_id: ${command.artifactId}`,
6606
+ `chart_id: ${command.chartId}`,
6607
+ `title: ${title}`,
6608
+ "dry_run: true",
6609
+ ]);
6610
+ return 0;
6611
+ }
6612
+ const payload = await requestAnalyticsApi({
6613
+ artifactId: command.artifactId,
6614
+ analyticsPath: "charts",
6615
+ resourceId: command.chartId,
6616
+ method: "PATCH",
6617
+ baseUrl: credentials.baseUrl,
6618
+ token: credentials.token,
6619
+ fetcher: runtime.fetcher ?? fetch,
6620
+ body,
6621
+ });
6622
+ printAnalyticsPayload(command, io, { ...payload, propertySlots }, [
6623
+ `app_id: ${command.artifactId}`,
6624
+ `chart_id: ${command.chartId}`,
6625
+ `title: ${String(payload.chart?.title ?? title)}`,
6626
+ `property_slots: ${propertySlots.length}`,
6627
+ ]);
6628
+ return 0;
6629
+ }
6630
+ async function runAnalyticsChartsDelete(command, io, runtime) {
6631
+ const { credentials, payload: dashboardsPayload } = await requestAnalyticsDashboardsForCli(command, runtime);
6632
+ const existing = findAnalyticsChart(dashboardsPayload, command.chartId);
6633
+ if (!existing) {
6634
+ throw new Error(`Analytics chart ${command.chartId} was not found for app ${command.artifactId}.`);
6635
+ }
6636
+ const dashboardId = readCliStringField(existing.dashboard, "id");
6637
+ if (command.dryRun) {
6638
+ printAnalyticsPayload(command, io, {
6639
+ ok: true,
6640
+ dryRun: true,
6641
+ artifactId: command.artifactId,
6642
+ chartId: command.chartId,
6643
+ dashboardId,
6644
+ currentChart: existing.chart,
6645
+ request: {
6646
+ chartId: command.chartId,
6647
+ },
6648
+ }, [
6649
+ `app_id: ${command.artifactId}`,
6650
+ `chart_id: ${command.chartId}`,
6651
+ "dry_run: true",
6652
+ ]);
6653
+ return 0;
6654
+ }
6655
+ const payload = await requestAnalyticsApi({
6656
+ artifactId: command.artifactId,
6657
+ analyticsPath: "charts",
6658
+ resourceId: command.chartId,
6659
+ method: "DELETE",
6660
+ baseUrl: credentials.baseUrl,
6661
+ token: credentials.token,
6662
+ fetcher: runtime.fetcher ?? fetch,
6663
+ });
6664
+ printAnalyticsPayload(command, io, payload, [
6665
+ `app_id: ${command.artifactId}`,
6666
+ `chart_id: ${command.chartId}`,
6667
+ `deleted: ${String(payload.deleted ?? "")}`,
5678
6668
  ]);
5679
6669
  return 0;
5680
6670
  }
@@ -5793,6 +6783,21 @@ export async function runTenderCli(args, io = {
5793
6783
  else if (command.topic === "app analytics charts create") {
5794
6784
  io.stdout(analyticsChartsCreateHelp());
5795
6785
  }
6786
+ else if (command.topic === "app analytics charts list") {
6787
+ io.stdout(analyticsChartsListHelp());
6788
+ }
6789
+ else if (command.topic === "app analytics charts update") {
6790
+ io.stdout(analyticsChartsUpdateHelp());
6791
+ }
6792
+ else if (command.topic === "app analytics charts delete") {
6793
+ io.stdout(analyticsChartsDeleteHelp());
6794
+ }
6795
+ else if (command.topic === "app analytics property-slots list") {
6796
+ io.stdout(analyticsPropertySlotsListHelp());
6797
+ }
6798
+ else if (command.topic === "app analytics property-slots activate") {
6799
+ io.stdout(analyticsPropertySlotsActivateHelp());
6800
+ }
5796
6801
  else if (command.topic === "app") {
5797
6802
  io.stdout(appHelp());
5798
6803
  }
@@ -5894,6 +6899,21 @@ export async function runTenderCli(args, io = {
5894
6899
  if (command.command === "app analytics charts create") {
5895
6900
  return await runAnalyticsChartsCreate(command, io, runtime);
5896
6901
  }
6902
+ if (command.command === "app analytics charts list") {
6903
+ return await runAnalyticsChartsList(command, io, runtime);
6904
+ }
6905
+ if (command.command === "app analytics charts update") {
6906
+ return await runAnalyticsChartsUpdate(command, io, runtime);
6907
+ }
6908
+ if (command.command === "app analytics charts delete") {
6909
+ return await runAnalyticsChartsDelete(command, io, runtime);
6910
+ }
6911
+ if (command.command === "app analytics property-slots list") {
6912
+ return await runAnalyticsPropertySlotsList(command, io, runtime);
6913
+ }
6914
+ if (command.command === "app analytics property-slots activate") {
6915
+ return await runAnalyticsPropertySlotsActivate(command, io, runtime);
6916
+ }
5897
6917
  if (command.command === "app git remote") {
5898
6918
  return await runGitRemote(command, io, runtime);
5899
6919
  }