@tenderprompt/cli 0.1.9 → 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 +50 -2
  2. package/dist/index.js +1397 -87
  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
@@ -266,7 +289,13 @@ Commands:
266
289
  tender app analytics suggestions <app-id> Generate starter chart suggestions
267
290
  tender app analytics dashboards <app-id> List or create dashboards
268
291
  tender app analytics export <kind> <app-id> Export catalog metadata or query rows
292
+ tender app analytics charts templates Print reusable chart spec templates
269
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
270
299
 
271
300
  Examples:
272
301
  tender app analytics summary artifact_123 --range 30d --json
@@ -277,7 +306,13 @@ Examples:
277
306
  tender app analytics export query artifact_123 --spec chart.json --format csv
278
307
  tender app analytics suggestions artifact_123 --range 2026-04-01..2026-04-30 --json
279
308
  tender app analytics dashboards artifact_123 --create "Agent dashboard" --json
280
- tender app analytics charts create artifact_123 --dashboard analytics_dashboard_123 --title "Signup funnel" --spec funnel.json --json`;
309
+ tender app analytics charts templates --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`;
281
316
  }
282
317
  function analyticsSummaryHelp() {
283
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]
@@ -340,19 +375,72 @@ Examples:
340
375
  tender app analytics dashboards artifact_123 --create "Agent dashboard" --dry-run --json
341
376
  tender app analytics dashboards artifact_123 --create "Agent dashboard" --json`;
342
377
  }
378
+ function analyticsChartsTemplatesHelp() {
379
+ return `Usage: tender app analytics charts templates [--json]
380
+
381
+ Print reusable chart spec templates for agents. Adapt templates to observed
382
+ events, paths, and property keys from analytics capabilities before querying or
383
+ saving.
384
+
385
+ Examples:
386
+ tender app analytics charts templates --json`;
387
+ }
343
388
  function analyticsChartsCreateHelp() {
344
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]
345
390
 
391
+ Property charts automatically activate required promoted property slots before
392
+ saving when the spec has an event_name = filter.
393
+
346
394
  Examples:
347
395
  tender app analytics charts create artifact_123 --dashboard analytics_dashboard_123 --title "Signup funnel" --spec funnel.json --dry-run --json
348
396
  tender app analytics charts create artifact_123 --dashboard analytics_dashboard_123 --title "Signup funnel" --spec funnel.json --json`;
349
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
+ }
350
438
  function artifactsListHelp() {
351
439
  return `Usage: tender app list [--base-url <url>] [--token <token>] [--profile <name>] [--json]
352
440
 
353
441
  Options:
354
442
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
355
- --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.
356
444
  --profile <name> Local config profile name. Defaults to the saved default profile.
357
445
  --json Emit stable JSON for coding agents.
358
446
 
@@ -369,7 +457,7 @@ Options:
369
457
  --target-pack <id> Target pack id. Defaults to the API default.
370
458
  --target-pack-version <value> Target pack version. Defaults to the API default.
371
459
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
372
- --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.
373
461
  --profile <name> Local config profile name. Defaults to the saved default profile.
374
462
  --json Emit stable JSON for coding agents.
375
463
 
@@ -383,7 +471,7 @@ function artifactsUpdateHelp() {
383
471
  Options:
384
472
  --name <name> New app name.
385
473
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
386
- --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.
387
475
  --profile <name> Local config profile name. Defaults to the saved default profile.
388
476
  --json Emit stable JSON for coding agents.
389
477
 
@@ -397,7 +485,7 @@ Options:
397
485
  --dir <path> Local checkout directory to create/update. Defaults to current directory.
398
486
  --remote <name> Git remote name to add or update. Defaults to tender.
399
487
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
400
- --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.
401
489
  --profile <name> Local config profile name. Defaults to the saved default profile.
402
490
  --ttl <value> Deprecated for Tender-hosted Git remotes; accepted for compatibility.
403
491
  --dry-run Show file and Git actions without writing.
@@ -415,7 +503,7 @@ function artifactsContextFetchHelp() {
415
503
  Options:
416
504
  --dir <path> Local checkout to update. Defaults to current directory.
417
505
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
418
- --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.
419
507
  --profile <name> Local config profile name. Defaults to the saved default profile.
420
508
  --dry-run Show create/overwrite/conflict actions without writing files.
421
509
  --force Overwrite conflicting managed context files.
@@ -432,7 +520,7 @@ function artifactsContextStatusHelp() {
432
520
  Options:
433
521
  --dir <path> Local checkout to inspect. Defaults to current directory.
434
522
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
435
- --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.
436
524
  --profile <name> Local config profile name. Defaults to the saved default profile.
437
525
  --json Emit stable JSON for coding agents.
438
526
 
@@ -445,7 +533,7 @@ function artifactsContextRefreshHelp() {
445
533
  Options:
446
534
  --dir <path> Local checkout to update. Defaults to current directory.
447
535
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
448
- --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.
449
537
  --profile <name> Local config profile name. Defaults to the saved default profile.
450
538
  --dry-run Show create/overwrite/conflict actions without writing files.
451
539
  --force Overwrite conflicting managed context files.
@@ -502,7 +590,7 @@ function gitRemoteHelp() {
502
590
 
503
591
  Options:
504
592
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
505
- --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.
506
594
  --profile <name> Local config profile name. Defaults to the saved default profile.
507
595
  --ttl <value> Deprecated for Tender-hosted Git remotes; accepted for compatibility.
508
596
  --json Emit stable JSON for coding agents.
@@ -519,7 +607,7 @@ Options:
519
607
  --dir <path> Local Git checkout to configure. Defaults to current directory.
520
608
  --remote <name> Git remote name to add or update. Defaults to tender.
521
609
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
522
- --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.
523
611
  --profile <name> Local config profile name. Defaults to the saved default profile.
524
612
  --ttl <value> Deprecated for Tender-hosted Git remotes; accepted for compatibility.
525
613
  --dry-run Show Git commands without running them.
@@ -547,7 +635,7 @@ function previewRebuildHelp() {
547
635
  Options:
548
636
  --commit <sha> Rebuild preview only if the app workspace is at this Git commit.
549
637
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
550
- --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.
551
639
  --profile <name> Local config profile name. Defaults to the saved default profile.
552
640
  --stream Request lifecycle events from the API.
553
641
  --json Emit stable JSON for coding agents.
@@ -561,7 +649,7 @@ function validateHelp() {
561
649
 
562
650
  Options:
563
651
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
564
- --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.
565
653
  --profile <name> Local config profile name. Defaults to the saved default profile.
566
654
  --stream Request lifecycle events from the API.
567
655
  --json Emit stable JSON for coding agents.
@@ -576,7 +664,7 @@ function buildHelp() {
576
664
  Options:
577
665
  --commit <sha> Build only if the app workspace is at this Git commit.
578
666
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
579
- --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.
580
668
  --profile <name> Local config profile name. Defaults to the saved default profile.
581
669
  --stream Request lifecycle events from the API.
582
670
  --json Emit stable JSON for coding agents.
@@ -592,7 +680,7 @@ Options:
592
680
  --commit <sha> Show the latest preview job for one artifact Git commit.
593
681
  --job <job-id> Show one lifecycle job directly.
594
682
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
595
- --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.
596
684
  --profile <name> Local config profile name. Defaults to the saved default profile.
597
685
  --json Emit stable JSON for coding agents.
598
686
 
@@ -607,7 +695,7 @@ Options:
607
695
  --commit <sha> Watch the latest preview job for one artifact Git commit.
608
696
  --job <job-id> Watch one lifecycle job directly.
609
697
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
610
- --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.
611
699
  --profile <name> Local config profile name. Defaults to the saved default profile.
612
700
  --stream Request Server-Sent Events from the API.
613
701
  --json Emit lifecycle events as JSON lines.
@@ -625,7 +713,7 @@ Options:
625
713
  --commit <sha> Publish only if the app workspace is at this Git commit.
626
714
  --message <text> Human audit message returned in CLI output.
627
715
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
628
- --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.
629
717
  --profile <name> Local config profile name. Defaults to the saved default profile.
630
718
  --stream Request lifecycle events from the API.
631
719
  --json Emit stable JSON for coding agents.
@@ -640,7 +728,7 @@ function publishStatusHelp() {
640
728
  Options:
641
729
  --job <job-id> Show one publish lifecycle job directly.
642
730
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
643
- --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.
644
732
  --profile <name> Local config profile name. Defaults to the saved default profile.
645
733
  --json Emit stable JSON for coding agents.
646
734
 
@@ -653,7 +741,7 @@ function publishWatchHelp() {
653
741
  Options:
654
742
  --job <job-id> Watch one publish lifecycle job directly.
655
743
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
656
- --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.
657
745
  --profile <name> Local config profile name. Defaults to the saved default profile.
658
746
  --stream Request Server-Sent Events from the API.
659
747
  --json Emit lifecycle events as JSON lines.
@@ -725,14 +813,23 @@ function tenderCliCapabilities() {
725
813
  when: "When understanding app analytics or creating saved charts.",
726
814
  commands: [
727
815
  "tender app analytics capabilities <artifact-id> --include-catalog --range 30d --json",
816
+ "tender app analytics charts templates --json",
728
817
  "tender app analytics query <artifact-id> --spec chart.json --json",
729
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",
730
821
  "tender app analytics dashboards <artifact-id> --create \"Agent dashboard\" --dry-run --json",
731
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",
732
826
  ],
733
827
  notes: [
734
828
  "Combine CLI capabilities with local source inspection of analytics calls.",
735
- "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.",
736
833
  ],
737
834
  },
738
835
  {
@@ -782,6 +879,52 @@ function tenderCliCapabilities() {
782
879
  requiresAuth: true,
783
880
  writes: false,
784
881
  },
882
+ {
883
+ command: "tender app analytics charts templates --json",
884
+ purpose: "Print reusable chart spec templates for agents to adapt to observed analytics data.",
885
+ requiresAuth: false,
886
+ writes: false,
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
+ },
785
928
  ],
786
929
  security: [
787
930
  "Prefer artifact-scoped device tokens.",
@@ -902,6 +1045,19 @@ async function resolveApiCredentials(input) {
902
1045
  profileName: null,
903
1046
  };
904
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
+ }
905
1061
  if (input.env.TENDER_API_TOKEN) {
906
1062
  return {
907
1063
  baseUrl: input.baseUrl ?? readDefaultBaseUrl(input.env),
@@ -911,7 +1067,7 @@ async function resolveApiCredentials(input) {
911
1067
  };
912
1068
  }
913
1069
  const config = await readConfig(input.env);
914
- const profileName = input.profileName ?? config.defaultProfile ?? DEFAULT_PROFILE_NAME;
1070
+ const profileName = config.defaultProfile ?? DEFAULT_PROFILE_NAME;
915
1071
  const profile = config.profiles?.[profileName];
916
1072
  if (!profile) {
917
1073
  throw new TenderCliUsageError("Tender API token is required. Run `tender auth login --device`, set TENDER_API_TOKEN, or pass --token <token>.");
@@ -2555,6 +2711,21 @@ function parseAnalyticsDashboardsArgs(args) {
2555
2711
  }
2556
2712
  return { command: "app analytics dashboards", artifactId, name, dryRun, ...parsed };
2557
2713
  }
2714
+ function parseAnalyticsChartsTemplatesArgs(args) {
2715
+ if (args.includes("--help") || args.includes("-h")) {
2716
+ return { command: "help", topic: "app analytics charts templates" };
2717
+ }
2718
+ let json = false;
2719
+ for (let index = 0; index < args.length; index += 1) {
2720
+ const arg = args[index];
2721
+ if (arg === "--json") {
2722
+ json = true;
2723
+ continue;
2724
+ }
2725
+ throw new TenderCliUsageError(`Unknown analytics charts templates option: ${arg}`);
2726
+ }
2727
+ return { command: "app analytics charts templates", json };
2728
+ }
2558
2729
  function parseAnalyticsExportCatalogArgs(args) {
2559
2730
  if (args.includes("--help") || args.includes("-h")) {
2560
2731
  return { command: "help", topic: "app analytics export catalog" };
@@ -2632,6 +2803,244 @@ function parseAnalyticsExportQueryArgs(args) {
2632
2803
  ...parsed,
2633
2804
  };
2634
2805
  }
2806
+ function analyticsChartTemplates() {
2807
+ return [
2808
+ {
2809
+ id: "daily_custom_events",
2810
+ title: "Daily custom events",
2811
+ visualization: "line",
2812
+ purpose: "Track app-owned business events over time.",
2813
+ usableAsIs: true,
2814
+ adaptWith: [
2815
+ "Use when the catalog has observed custom events.",
2816
+ "Switch the range preset to 7d or 90d when the user's question asks for that window.",
2817
+ ],
2818
+ spec: {
2819
+ version: 1,
2820
+ dataset: "events",
2821
+ visualization: "line",
2822
+ metric: { name: "custom_event_count" },
2823
+ groupBy: [{ field: "day" }],
2824
+ filters: [],
2825
+ range: { preset: "30d" },
2826
+ sort: [{ field: "label", direction: "asc" }],
2827
+ limit: 30,
2828
+ },
2829
+ },
2830
+ {
2831
+ id: "top_custom_events",
2832
+ title: "Top custom events",
2833
+ visualization: "bar",
2834
+ purpose: "Show which app-owned events happen most often.",
2835
+ usableAsIs: true,
2836
+ adaptWith: [
2837
+ "Use this before proposing deeper charts so the agent can see the dominant event shape.",
2838
+ ],
2839
+ spec: {
2840
+ version: 1,
2841
+ dataset: "events",
2842
+ visualization: "bar",
2843
+ metric: { name: "custom_event_count" },
2844
+ groupBy: [{ field: "event_name" }],
2845
+ filters: [],
2846
+ range: { preset: "30d" },
2847
+ sort: [{ field: "value", direction: "desc" }],
2848
+ limit: 10,
2849
+ },
2850
+ },
2851
+ {
2852
+ id: "top_request_paths",
2853
+ title: "Top request paths",
2854
+ visualization: "bar",
2855
+ purpose: "Find the generated app routes receiving the most traffic.",
2856
+ usableAsIs: true,
2857
+ adaptWith: [
2858
+ "Use only when the capabilities catalog reports request paths.",
2859
+ "Inspect the local source to understand what the busiest routes represent.",
2860
+ ],
2861
+ spec: {
2862
+ version: 1,
2863
+ dataset: "events",
2864
+ visualization: "bar",
2865
+ metric: { name: "request_count" },
2866
+ groupBy: [{ field: "path" }],
2867
+ filters: [],
2868
+ range: { preset: "30d" },
2869
+ sort: [{ field: "value", direction: "desc" }],
2870
+ limit: 10,
2871
+ },
2872
+ },
2873
+ {
2874
+ id: "event_funnel_by_name",
2875
+ title: "Event funnel",
2876
+ visualization: "funnel",
2877
+ purpose: "Measure ordered conversion through observed custom event names.",
2878
+ usableAsIs: false,
2879
+ placeholders: {
2880
+ eventNames: ["<start_event>", "<middle_event>", "<conversion_event>"],
2881
+ },
2882
+ adaptWith: [
2883
+ "Replace placeholders with ordered event names from catalog classification or observed events.",
2884
+ "Use only event names observed for the selected app.",
2885
+ "Validate with analytics query before saving because funnel order changes the meaning.",
2886
+ ],
2887
+ spec: {
2888
+ version: 1,
2889
+ dataset: "events",
2890
+ visualization: "funnel",
2891
+ metric: { name: "custom_event_count" },
2892
+ groupBy: [{ field: "event_name" }],
2893
+ filters: [
2894
+ {
2895
+ field: "event_name",
2896
+ op: "in",
2897
+ value: ["<start_event>", "<middle_event>", "<conversion_event>"],
2898
+ },
2899
+ ],
2900
+ range: { preset: "30d" },
2901
+ sort: [{ field: "label", direction: "asc" }],
2902
+ limit: 3,
2903
+ },
2904
+ },
2905
+ {
2906
+ id: "request_path_funnel",
2907
+ title: "Request path funnel",
2908
+ visualization: "funnel",
2909
+ purpose: "Measure ordered conversion through observed request paths.",
2910
+ usableAsIs: false,
2911
+ placeholders: {
2912
+ paths: ["/<start_path>", "/<middle_path>", "/<conversion_path>"],
2913
+ },
2914
+ adaptWith: [
2915
+ "Use when catalog classification includes a path-based funnel or the source code shows ordered route milestones.",
2916
+ "Replace placeholders with exact observed paths from the selected app.",
2917
+ "Do not include repeated activity routes unless source context says they are milestones.",
2918
+ ],
2919
+ spec: {
2920
+ version: 1,
2921
+ dataset: "events",
2922
+ visualization: "funnel",
2923
+ metric: { name: "request_count" },
2924
+ groupBy: [{ field: "path" }],
2925
+ filters: [
2926
+ {
2927
+ field: "path",
2928
+ op: "in",
2929
+ value: ["/<start_path>", "/<middle_path>", "/<conversion_path>"],
2930
+ },
2931
+ ],
2932
+ range: { preset: "30d" },
2933
+ sort: [{ field: "label", direction: "asc" }],
2934
+ limit: 3,
2935
+ },
2936
+ },
2937
+ {
2938
+ id: "property_breakdown",
2939
+ title: "Property breakdown",
2940
+ visualization: "bar",
2941
+ purpose: "Compare a promoted dimension property observed in events.",
2942
+ usableAsIs: false,
2943
+ placeholders: {
2944
+ property: "<promoted_dimension_property>",
2945
+ },
2946
+ adaptWith: [
2947
+ "Use a property key promoted by capabilities chartSpec.groupBy, such as property:plan.",
2948
+ "Choose stable categorical properties with enough observations to compare.",
2949
+ ],
2950
+ spec: {
2951
+ version: 1,
2952
+ dataset: "events",
2953
+ visualization: "bar",
2954
+ metric: { name: "custom_event_count" },
2955
+ groupBy: [{ field: "property:<promoted_dimension_property>" }],
2956
+ filters: [],
2957
+ range: { preset: "30d" },
2958
+ sort: [{ field: "value", direction: "desc" }],
2959
+ limit: 10,
2960
+ },
2961
+ },
2962
+ {
2963
+ id: "average_metric_property",
2964
+ title: "Average metric property",
2965
+ visualization: "line",
2966
+ purpose: "Track an observed numeric event property over time.",
2967
+ usableAsIs: false,
2968
+ placeholders: {
2969
+ metric: "avg:<promoted_metric_property>",
2970
+ },
2971
+ adaptWith: [
2972
+ "Use a numeric property key promoted by capabilities chartSpec.metrics.",
2973
+ "Prefer averages for scores, durations, quantities, or ordinal values.",
2974
+ ],
2975
+ spec: {
2976
+ version: 1,
2977
+ dataset: "events",
2978
+ visualization: "line",
2979
+ metric: { name: "avg:<promoted_metric_property>" },
2980
+ groupBy: [{ field: "day" }],
2981
+ filters: [],
2982
+ range: { preset: "30d" },
2983
+ sort: [{ field: "label", direction: "asc" }],
2984
+ limit: 30,
2985
+ },
2986
+ },
2987
+ {
2988
+ id: "single_number_total",
2989
+ title: "Single number total",
2990
+ visualization: "number",
2991
+ purpose: "Show one headline count for a selected event or request segment.",
2992
+ usableAsIs: false,
2993
+ placeholders: {
2994
+ eventName: "<event_name>",
2995
+ },
2996
+ adaptWith: [
2997
+ "Use when a dashboard needs a compact KPI card.",
2998
+ "Replace the event filter with an observed conversion or outcome event.",
2999
+ ],
3000
+ spec: {
3001
+ version: 1,
3002
+ dataset: "events",
3003
+ visualization: "number",
3004
+ metric: { name: "custom_event_count" },
3005
+ groupBy: [],
3006
+ filters: [{ field: "event_name", op: "=", value: "<event_name>" }],
3007
+ range: { preset: "30d" },
3008
+ sort: [],
3009
+ limit: 1,
3010
+ },
3011
+ },
3012
+ ];
3013
+ }
3014
+ function runAnalyticsChartsTemplates(command, io) {
3015
+ const templates = analyticsChartTemplates();
3016
+ const payload = {
3017
+ ok: true,
3018
+ command: command.command,
3019
+ templates,
3020
+ next: [
3021
+ "Run tender app analytics capabilities <artifact-id> --include-catalog --range 30d --json.",
3022
+ "Choose a template and replace placeholders with observed events, paths, or promoted property keys.",
3023
+ "Validate with tender app analytics query <artifact-id> --spec chart.json --json.",
3024
+ "Preview saved writes with tender app analytics dashboards ... --dry-run and charts create ... --dry-run.",
3025
+ ],
3026
+ };
3027
+ if (command.json) {
3028
+ io.stdout(JSON.stringify(payload, null, 2));
3029
+ }
3030
+ else {
3031
+ io.stdout([
3032
+ "Analytics chart templates",
3033
+ "",
3034
+ ...templates.map((template) => {
3035
+ const marker = template.usableAsIs ? "usable as-is" : "adapt before use";
3036
+ return `- ${template.id}: ${template.title} (${marker})`;
3037
+ }),
3038
+ "",
3039
+ "Use --json to copy full chart specs.",
3040
+ ].join("\n"));
3041
+ }
3042
+ return 0;
3043
+ }
2635
3044
  function parseAnalyticsChartsCreateArgs(args) {
2636
3045
  if (args.includes("--help") || args.includes("-h")) {
2637
3046
  return { command: "help", topic: "app analytics charts create" };
@@ -2681,67 +3090,276 @@ function parseAnalyticsChartsCreateArgs(args) {
2681
3090
  ...parsed,
2682
3091
  };
2683
3092
  }
2684
- function parseTenderArgs(args) {
2685
- if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
2686
- return { command: "help", topic: "root" };
3093
+ function parseAnalyticsChartsListArgs(args) {
3094
+ if (args.includes("--help") || args.includes("-h")) {
3095
+ return { command: "help", topic: "app analytics charts list" };
2687
3096
  }
2688
- if (args[0] === "capabilities") {
2689
- return parseCapabilitiesArgs(args.slice(1));
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");
2690
3100
  }
2691
- if (args[0] === "auth") {
2692
- if (args[1] === undefined || args[1] === "--help" || args[1] === "-h") {
2693
- return { command: "help", topic: "auth" };
2694
- }
2695
- if (args[1] === "login") {
2696
- return parseAuthLoginArgs(args.slice(2));
2697
- }
2698
- if (args[1] === "status") {
2699
- return parseAuthStatusArgs(args.slice(2));
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;
2700
3109
  }
2701
- throw new TenderCliUsageError(`Unknown auth command: ${args[1]}`);
3110
+ throw new TenderCliUsageError(`Unknown analytics charts list option: ${arg}`);
2702
3111
  }
2703
- if (args[0] === "apps") {
2704
- throw new TenderCliUsageError("Unknown command: apps. Use `tender app ...` for Tender App workflows.");
3112
+ if (!dashboardId) {
3113
+ throw new TenderCliUsageError("--dashboard is required. Example: tender app analytics charts list artifact_123 --dashboard analytics_dashboard_123 --json");
2705
3114
  }
2706
- if (args[0] !== "app") {
2707
- throw new TenderCliUsageError(`Unknown command: ${args[0]}`);
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" };
2708
3125
  }
2709
- const resourceArgs = normalizeAppArgs(args);
2710
- if (resourceArgs[1] === undefined ||
2711
- resourceArgs[1] === "--help" ||
2712
- resourceArgs[1] === "-h") {
2713
- return { command: "help", topic: "app" };
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");
2714
3129
  }
2715
- if (resourceArgs[1] !== "doctor") {
2716
- if (resourceArgs[1] === "list") {
2717
- return parseArtifactsListArgs(resourceArgs.slice(2));
2718
- }
2719
- if (resourceArgs[1] === "create") {
2720
- return parseArtifactsCreateArgs(resourceArgs.slice(2));
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;
2721
3141
  }
2722
- if (resourceArgs[1] === "update") {
2723
- return parseArtifactsUpdateArgs(resourceArgs.slice(2));
3142
+ if (arg === "--title") {
3143
+ title = parseRequiredFlagValue(parsed.remaining[index + 1], "--title");
3144
+ index += 1;
3145
+ continue;
2724
3146
  }
2725
- if (resourceArgs[1] === "init") {
2726
- return parseArtifactsInitArgs(resourceArgs.slice(2));
3147
+ if (arg === "--spec") {
3148
+ specPath = parseSpecPath(parsed.remaining[index + 1]);
3149
+ index += 1;
3150
+ continue;
2727
3151
  }
2728
- if (resourceArgs[1] === "context" && resourceArgs[2] === "fetch") {
2729
- return parseContextFetchArgs(resourceArgs.slice(3));
3152
+ if (arg === "--dry-run") {
3153
+ dryRun = true;
3154
+ continue;
2730
3155
  }
2731
- if (resourceArgs[1] === "context" && resourceArgs[2] === "status") {
2732
- return parseContextStatusArgs(resourceArgs.slice(3));
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;
2733
3191
  }
2734
- if (resourceArgs[1] === "context" && resourceArgs[2] === "refresh") {
2735
- return parseContextRefreshArgs(resourceArgs.slice(3));
3192
+ if (arg === "--dry-run") {
3193
+ dryRun = true;
3194
+ continue;
2736
3195
  }
2737
- if (resourceArgs[1] === "bindings" && resourceArgs[2] === "outbound") {
2738
- return parseBindingsOutboundArgs(resourceArgs.slice(3));
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;
2739
3225
  }
2740
- if (resourceArgs[1] === "bindings" &&
2741
- (resourceArgs[2] === undefined ||
2742
- resourceArgs[2] === "--help" ||
2743
- resourceArgs[2] === "-h")) {
2744
- return { command: "help", topic: "app bindings outbound" };
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
+ }
3302
+ function parseTenderArgs(args) {
3303
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
3304
+ return { command: "help", topic: "root" };
3305
+ }
3306
+ if (args[0] === "capabilities") {
3307
+ return parseCapabilitiesArgs(args.slice(1));
3308
+ }
3309
+ if (args[0] === "auth") {
3310
+ if (args[1] === undefined || args[1] === "--help" || args[1] === "-h") {
3311
+ return { command: "help", topic: "auth" };
3312
+ }
3313
+ if (args[1] === "login") {
3314
+ return parseAuthLoginArgs(args.slice(2));
3315
+ }
3316
+ if (args[1] === "status") {
3317
+ return parseAuthStatusArgs(args.slice(2));
3318
+ }
3319
+ throw new TenderCliUsageError(`Unknown auth command: ${args[1]}`);
3320
+ }
3321
+ if (args[0] === "apps") {
3322
+ throw new TenderCliUsageError("Unknown command: apps. Use `tender app ...` for Tender App workflows.");
3323
+ }
3324
+ if (args[0] !== "app") {
3325
+ throw new TenderCliUsageError(`Unknown command: ${args[0]}`);
3326
+ }
3327
+ const resourceArgs = normalizeAppArgs(args);
3328
+ if (resourceArgs[1] === undefined ||
3329
+ resourceArgs[1] === "--help" ||
3330
+ resourceArgs[1] === "-h") {
3331
+ return { command: "help", topic: "app" };
3332
+ }
3333
+ if (resourceArgs[1] !== "doctor") {
3334
+ if (resourceArgs[1] === "list") {
3335
+ return parseArtifactsListArgs(resourceArgs.slice(2));
3336
+ }
3337
+ if (resourceArgs[1] === "create") {
3338
+ return parseArtifactsCreateArgs(resourceArgs.slice(2));
3339
+ }
3340
+ if (resourceArgs[1] === "update") {
3341
+ return parseArtifactsUpdateArgs(resourceArgs.slice(2));
3342
+ }
3343
+ if (resourceArgs[1] === "init") {
3344
+ return parseArtifactsInitArgs(resourceArgs.slice(2));
3345
+ }
3346
+ if (resourceArgs[1] === "context" && resourceArgs[2] === "fetch") {
3347
+ return parseContextFetchArgs(resourceArgs.slice(3));
3348
+ }
3349
+ if (resourceArgs[1] === "context" && resourceArgs[2] === "status") {
3350
+ return parseContextStatusArgs(resourceArgs.slice(3));
3351
+ }
3352
+ if (resourceArgs[1] === "context" && resourceArgs[2] === "refresh") {
3353
+ return parseContextRefreshArgs(resourceArgs.slice(3));
3354
+ }
3355
+ if (resourceArgs[1] === "bindings" && resourceArgs[2] === "outbound") {
3356
+ return parseBindingsOutboundArgs(resourceArgs.slice(3));
3357
+ }
3358
+ if (resourceArgs[1] === "bindings" &&
3359
+ (resourceArgs[2] === undefined ||
3360
+ resourceArgs[2] === "--help" ||
3361
+ resourceArgs[2] === "-h")) {
3362
+ return { command: "help", topic: "app bindings outbound" };
2745
3363
  }
2746
3364
  if (resourceArgs[1] === "analytics" &&
2747
3365
  (resourceArgs[2] === undefined ||
@@ -2764,6 +3382,23 @@ function parseTenderArgs(args) {
2764
3382
  if (resourceArgs[1] === "analytics" && resourceArgs[2] === "dashboards") {
2765
3383
  return parseAnalyticsDashboardsArgs(resourceArgs.slice(3));
2766
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
+ }
2767
3402
  if (resourceArgs[1] === "analytics" &&
2768
3403
  resourceArgs[2] === "export" &&
2769
3404
  (resourceArgs[3] === undefined ||
@@ -2781,11 +3416,38 @@ function parseTenderArgs(args) {
2781
3416
  resourceArgs[3] === "query") {
2782
3417
  return parseAnalyticsExportQueryArgs(resourceArgs.slice(4));
2783
3418
  }
3419
+ if (resourceArgs[1] === "analytics" &&
3420
+ resourceArgs[2] === "charts" &&
3421
+ resourceArgs[3] === "templates") {
3422
+ return parseAnalyticsChartsTemplatesArgs(resourceArgs.slice(4));
3423
+ }
3424
+ if (resourceArgs[1] === "analytics" &&
3425
+ resourceArgs[2] === "charts" &&
3426
+ (resourceArgs[3] === undefined ||
3427
+ resourceArgs[3] === "--help" ||
3428
+ resourceArgs[3] === "-h")) {
3429
+ return { command: "help", topic: "app analytics charts templates" };
3430
+ }
2784
3431
  if (resourceArgs[1] === "analytics" &&
2785
3432
  resourceArgs[2] === "charts" &&
2786
3433
  resourceArgs[3] === "create") {
2787
3434
  return parseAnalyticsChartsCreateArgs(resourceArgs.slice(4));
2788
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
+ }
2789
3451
  if (resourceArgs[1] === "context" &&
2790
3452
  (resourceArgs[2] === undefined ||
2791
3453
  resourceArgs[2] === "--help" ||
@@ -3158,7 +3820,9 @@ async function requestAnalyticsApi(input) {
3158
3820
  if (input.includeCatalog) {
3159
3821
  params.set("includeCatalog", "1");
3160
3822
  }
3161
- const suffix = input.analyticsPath ? `/${input.analyticsPath}` : "";
3823
+ const suffix = input.analyticsPath
3824
+ ? `/${input.analyticsPath}${input.resourceId ? `/${encodeURIComponent(input.resourceId)}` : ""}`
3825
+ : "";
3162
3826
  const query = params.size > 0 ? `?${params.toString()}` : "";
3163
3827
  const response = await input.fetcher(`${input.baseUrl}/api/v1/artifacts/${encodeURIComponent(input.artifactId)}/analytics${suffix}${query}`, {
3164
3828
  method: input.method,
@@ -3176,12 +3840,12 @@ async function requestAnalyticsApi(input) {
3176
3840
  const message = (typeof payload?.message === "string" && payload.message) ||
3177
3841
  (typeof payload?.reasonCode === "string" && payload.reasonCode) ||
3178
3842
  `Analytics request failed with HTTP ${response.status}.`;
3179
- throw new Error([
3843
+ throw new TenderCliAnalyticsApiError([
3180
3844
  message,
3181
3845
  `Scope: artifact ${input.artifactId}, analytics/${input.analyticsPath || "summary"}.`,
3182
3846
  `Reason: ${reasonCode}.`,
3183
3847
  `Example: ${analyticsCorrectedExample(input.artifactId, input.analyticsPath)}.`,
3184
- ].join("\n"));
3848
+ ].join("\n"), reasonCode);
3185
3849
  }
3186
3850
  return payload;
3187
3851
  }
@@ -3195,6 +3859,9 @@ function analyticsCorrectedExample(artifactId, analyticsPath) {
3195
3859
  if (analyticsPath === "suggestions") {
3196
3860
  return `tender app analytics suggestions ${artifactId} --range 30d --json`;
3197
3861
  }
3862
+ if (analyticsPath === "property-slots") {
3863
+ return `tender app analytics property-slots list ${artifactId} --json`;
3864
+ }
3198
3865
  if (analyticsPath === "dashboards") {
3199
3866
  return `tender app analytics dashboards ${artifactId} --json`;
3200
3867
  }
@@ -3203,6 +3870,332 @@ function analyticsCorrectedExample(artifactId, analyticsPath) {
3203
3870
  }
3204
3871
  return `tender app analytics summary ${artifactId} --range 30d --json`;
3205
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
+ }
3206
4199
  function printAnalyticsPayload(command, io, payload, fallbackLines) {
3207
4200
  if (command.json) {
3208
4201
  io.stdout(JSON.stringify(payload, null, 2));
@@ -3491,6 +4484,47 @@ async function planSourceFiles(input) {
3491
4484
  }
3492
4485
  async function runAuthStatus(command, io, runtime) {
3493
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
+ }
3494
4528
  if (env.TENDER_API_TOKEN) {
3495
4529
  const result = {
3496
4530
  ok: true,
@@ -5207,14 +6241,11 @@ async function runAnalyticsQuery(command, io, runtime) {
5207
6241
  specPath: command.specPath,
5208
6242
  stdin: runtime.stdin,
5209
6243
  });
5210
- const payload = await requestAnalyticsApi({
5211
- artifactId: command.artifactId,
5212
- analyticsPath: "query",
5213
- method: "POST",
5214
- baseUrl: credentials.baseUrl,
5215
- token: credentials.token,
5216
- fetcher: runtime.fetcher ?? fetch,
5217
- body: { spec },
6244
+ const payload = await requestAnalyticsQueryWithPropertyGuidance({
6245
+ command,
6246
+ credentials,
6247
+ runtime,
6248
+ spec,
5218
6249
  });
5219
6250
  const rows = Array.isArray(payload.rows) ? payload.rows : [];
5220
6251
  printAnalyticsPayload(command, io, payload, [
@@ -5335,21 +6366,119 @@ async function runAnalyticsExportQuery(command, io, runtime) {
5335
6366
  specPath: command.specPath,
5336
6367
  stdin: runtime.stdin,
5337
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);
5338
6385
  const payload = await requestAnalyticsApi({
5339
6386
  artifactId: command.artifactId,
5340
- analyticsPath: "query",
5341
- method: "POST",
6387
+ analyticsPath: "property-slots",
6388
+ method: "GET",
5342
6389
  baseUrl: credentials.baseUrl,
5343
6390
  token: credentials.token,
5344
6391
  fetcher: runtime.fetcher ?? fetch,
5345
- body: { spec },
5346
6392
  });
5347
- const rows = Array.isArray(payload.rows) ? payload.rows : [];
5348
- if (command.format === "csv") {
5349
- 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
+ ]);
5350
6437
  return 0;
5351
6438
  }
5352
- 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
+ ]);
5353
6482
  return 0;
5354
6483
  }
5355
6484
  async function runAnalyticsChartsCreate(command, io, runtime) {
@@ -5363,11 +6492,25 @@ async function runAnalyticsChartsCreate(command, io, runtime) {
5363
6492
  spec,
5364
6493
  layout: {},
5365
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
+ : [];
5366
6508
  if (command.dryRun) {
5367
6509
  const payload = {
5368
6510
  ok: true,
5369
6511
  dryRun: true,
5370
6512
  artifactId: command.artifactId,
6513
+ propertySlots,
5371
6514
  request: body,
5372
6515
  };
5373
6516
  printAnalyticsPayload(command, io, payload, [
@@ -5378,7 +6521,9 @@ async function runAnalyticsChartsCreate(command, io, runtime) {
5378
6521
  ]);
5379
6522
  return 0;
5380
6523
  }
5381
- const credentials = await resolveAnalyticsCredentials(command, runtime);
6524
+ if (!credentials) {
6525
+ throw new Error("Analytics credentials are required to create saved charts.");
6526
+ }
5382
6527
  const payload = await requestAnalyticsApi({
5383
6528
  artifactId: command.artifactId,
5384
6529
  analyticsPath: "charts",
@@ -5388,9 +6533,138 @@ async function runAnalyticsChartsCreate(command, io, runtime) {
5388
6533
  fetcher: runtime.fetcher ?? fetch,
5389
6534
  body,
5390
6535
  });
5391
- printAnalyticsPayload(command, io, payload, [
6536
+ printAnalyticsPayload(command, io, { ...payload, propertySlots }, [
5392
6537
  `app_id: ${command.artifactId}`,
5393
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 ?? "")}`,
5394
6668
  ]);
5395
6669
  return 0;
5396
6670
  }
@@ -5503,9 +6777,27 @@ export async function runTenderCli(args, io = {
5503
6777
  else if (command.topic === "app analytics export query") {
5504
6778
  io.stdout(analyticsExportQueryHelp());
5505
6779
  }
6780
+ else if (command.topic === "app analytics charts templates") {
6781
+ io.stdout(analyticsChartsTemplatesHelp());
6782
+ }
5506
6783
  else if (command.topic === "app analytics charts create") {
5507
6784
  io.stdout(analyticsChartsCreateHelp());
5508
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
+ }
5509
6801
  else if (command.topic === "app") {
5510
6802
  io.stdout(appHelp());
5511
6803
  }
@@ -5601,9 +6893,27 @@ export async function runTenderCli(args, io = {
5601
6893
  if (command.command === "app analytics export query") {
5602
6894
  return await runAnalyticsExportQuery(command, io, runtime);
5603
6895
  }
6896
+ if (command.command === "app analytics charts templates") {
6897
+ return runAnalyticsChartsTemplates(command, io);
6898
+ }
5604
6899
  if (command.command === "app analytics charts create") {
5605
6900
  return await runAnalyticsChartsCreate(command, io, runtime);
5606
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
+ }
5607
6917
  if (command.command === "app git remote") {
5608
6918
  return await runGitRemote(command, io, runtime);
5609
6919
  }