@monster-orange/den 0.2.7 → 0.3.0

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 (139) hide show
  1. package/CHANGELOG.md +1655 -0
  2. package/README.md +4 -2
  3. package/bin/den +2 -1
  4. package/docs/distributions.md +619 -3
  5. package/make/den.mk +19 -4
  6. package/make/journeys.mk +2 -1
  7. package/make/local_dev.mk +6 -13
  8. package/make/secrets-1password.mk +18 -0
  9. package/make/test.mk +1 -1
  10. package/package.json +9 -2
  11. package/schemas/den.plugin.v1.json +156 -0
  12. package/schemas/den.theme.v1.json +269 -0
  13. package/scripts/auth-configure.ts +16 -8
  14. package/scripts/heal-config.ts +0 -5
  15. package/scripts/setup-1password.ts +41 -19
  16. package/scripts/setup-secrets.ts +67 -101
  17. package/src/pillars/api/global-setup.ts +1 -1
  18. package/src/pillars/journeys/sandbox.ts +23 -18
  19. package/src/pillars/journeys/tools/browser-session.ts +2 -2
  20. package/src/scripts/setup-default-workspace.ts +2 -2
  21. package/src/shared/agent-actor.ts +38 -3
  22. package/src/shared/auth/headless-login.ts +1 -1
  23. package/src/shared/auth/sudo-vault.ts +93 -31
  24. package/src/shared/auth/token-providers/index.ts +7 -3
  25. package/src/shared/brand.ts +7 -0
  26. package/src/shared/config-error.ts +63 -0
  27. package/src/shared/constants.ts +3 -24
  28. package/src/shared/den-config.ts +185 -7
  29. package/src/shared/edge/config.ts +147 -21
  30. package/src/shared/edge/exec.ts +31 -0
  31. package/src/shared/edge/included.ts +169 -0
  32. package/src/shared/edge/render.ts +69 -14
  33. package/src/shared/edge/runtime.ts +128 -121
  34. package/src/shared/edge/status.ts +68 -8
  35. package/src/shared/edge/tls.ts +328 -0
  36. package/src/shared/edge/types.ts +25 -9
  37. package/src/shared/environment-manager/catalog.ts +52 -1
  38. package/src/shared/environment-manager/default-workspace.ts +26 -3
  39. package/src/shared/environment-manager/git-remote.ts +49 -0
  40. package/src/shared/environment-manager/health.ts +73 -38
  41. package/src/shared/environment-manager/scope.ts +168 -0
  42. package/src/shared/environment-manager/service-runner.ts +789 -43
  43. package/src/shared/environment-manager/up-targets.ts +132 -0
  44. package/src/shared/environment-manager/workspace-manager.ts +67 -25
  45. package/src/shared/environment-manager/workspace-org.ts +26 -11
  46. package/src/shared/health/rollup.ts +48 -0
  47. package/src/shared/manifest/service-links.ts +20 -8
  48. package/src/shared/open-url.ts +21 -2
  49. package/src/shared/org-sources.ts +328 -0
  50. package/src/shared/org.ts +53 -5
  51. package/src/shared/plugins/manifest.ts +406 -0
  52. package/src/shared/plugins/store.ts +358 -0
  53. package/src/shared/process/legacy-adopt.ts +285 -0
  54. package/src/shared/process/process-group.ts +202 -0
  55. package/src/shared/process/spawn-streaming.ts +7 -0
  56. package/src/shared/secrets/doppler.ts +1 -1
  57. package/src/shared/secrets/index.ts +279 -50
  58. package/src/shared/secrets/none.ts +56 -0
  59. package/src/shared/secrets/strategies.ts +202 -0
  60. package/src/shared/secrets/types.ts +1 -1
  61. package/src/shared/smoke/git-remote.ts +40 -0
  62. package/src/shared/stacks/manifest-commands.ts +1 -1
  63. package/src/shared/stacks/namespace.ts +178 -0
  64. package/src/shared/stacks/owners.ts +303 -0
  65. package/src/shared/stacks/render.ts +8 -3
  66. package/src/shared/stacks/runtime.ts +100 -16
  67. package/src/shared/stacks/status.ts +63 -12
  68. package/src/shared/strategies/registry.ts +89 -0
  69. package/src/shared/theme/contract.ts +538 -0
  70. package/src/shared/types/index.ts +24 -5
  71. package/src/ui/cli/clone-plan.ts +154 -23
  72. package/src/ui/cli/create.ts +24 -7
  73. package/src/ui/cli/doctor-command.ts +131 -0
  74. package/src/ui/cli/edge-command.ts +56 -10
  75. package/src/ui/cli/flag-parser.ts +62 -3
  76. package/src/ui/cli/health-display.ts +25 -0
  77. package/src/ui/cli/index.ts +649 -72
  78. package/src/ui/cli/notice.ts +20 -0
  79. package/src/ui/cli/org-command.ts +12 -2
  80. package/src/ui/cli/plugin-command.ts +849 -0
  81. package/src/ui/cli/stack-command.ts +63 -2
  82. package/src/ui/cli/test-runner.ts +11 -9
  83. package/src/ui/cli/theme-command.ts +136 -0
  84. package/src/ui/cli/up-args.ts +15 -0
  85. package/src/ui/cli/web-command.ts +47 -23
  86. package/src/ui/mcp/api.ts +28 -2
  87. package/src/ui/mcp/scope.ts +298 -0
  88. package/src/ui/mcp/server.ts +70 -8
  89. package/src/ui/mcp/tools.ts +151 -24
  90. package/src/ui/server/appearance-prefs.ts +95 -0
  91. package/src/ui/server/audit.ts +90 -3
  92. package/src/ui/server/daemon-reaper.ts +29 -1
  93. package/src/ui/server/engine-stamp.ts +83 -0
  94. package/src/ui/server/github-service.ts +29 -45
  95. package/src/ui/server/graph-page.ts +20 -0
  96. package/src/ui/server/graph-prefs.ts +44 -1
  97. package/src/ui/server/http-errors.ts +293 -0
  98. package/src/ui/server/http-helpers.ts +22 -3
  99. package/src/ui/server/index.ts +590 -45
  100. package/src/ui/server/public/appearance.js +531 -0
  101. package/src/ui/server/public/edge-page.js +248 -0
  102. package/src/ui/server/public/edge.html +251 -0
  103. package/src/ui/server/public/environments.html +65 -83
  104. package/src/ui/server/public/error-page.js +247 -0
  105. package/src/ui/server/public/error.html +260 -0
  106. package/src/ui/server/public/graph-model.js +332 -14
  107. package/src/ui/server/public/graph.css +141 -47
  108. package/src/ui/server/public/graph.js +223 -44
  109. package/src/ui/server/public/header.js +396 -600
  110. package/src/ui/server/public/health-fit.js +248 -0
  111. package/src/ui/server/public/health-rollup.js +128 -0
  112. package/src/ui/server/public/i18n/es.js +287 -0
  113. package/src/ui/server/public/i18n/ja.js +276 -0
  114. package/src/ui/server/public/index.html +139 -438
  115. package/src/ui/server/public/local-dev.html +3468 -4044
  116. package/src/ui/server/public/logs.html +71 -208
  117. package/src/ui/server/public/panel.js +15 -3
  118. package/src/ui/server/public/release.html +175 -389
  119. package/src/ui/server/public/services.html +596 -770
  120. package/src/ui/server/public/settings-page.js +759 -0
  121. package/src/ui/server/public/settings-registry.js +521 -0
  122. package/src/ui/server/public/settings.html +590 -0
  123. package/src/ui/server/public/shell.js +3869 -0
  124. package/src/ui/server/public/topbar.js +1413 -0
  125. package/src/ui/server/public/version-label.js +12 -0
  126. package/src/ui/server/public/workspace-picker.js +74 -34
  127. package/src/ui/server/readonly-routes.ts +24 -4
  128. package/src/ui/server/release-service.ts +25 -3
  129. package/src/ui/server/services-list.ts +94 -17
  130. package/src/ui/server/settings-distribution.ts +74 -0
  131. package/src/ui/server/shell-prefs.ts +192 -0
  132. package/src/ui/server/theme-routes.ts +292 -0
  133. package/src/ui/server/theme-runtime.ts +407 -0
  134. package/src/ui/server/ui-assets.ts +16 -1
  135. package/src/ui/server/ui-language.ts +50 -0
  136. package/src/ui/server/workspace-routes.ts +55 -9
  137. package/themes/den/den-theme.json +72 -0
  138. package/themes/den/favicon.ico +0 -0
  139. package/themes/den/mark.svg +6 -0
@@ -3,8 +3,30 @@
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>__DEN_TITLE__</title>
7
- <link rel="icon" href="/favicon.ico?v=2" />
6
+ <script>
7
+ // Mode before first paint: yours (the server fills it in below),
8
+ // else the system's, else the theme's default, else dark.
9
+ (function () {
10
+ var m = "@@DEN_MODE@@";
11
+ if (m !== "dark" && m !== "light") {
12
+ var q = function (s) {
13
+ return (
14
+ window.matchMedia &&
15
+ matchMedia("(prefers-color-scheme: " + s + ")").matches
16
+ );
17
+ };
18
+ m = q("light")
19
+ ? "light"
20
+ : q("dark")
21
+ ? "dark"
22
+ : "@@DEN_DEFAULT_MODE@@";
23
+ }
24
+ document.documentElement.dataset.mode =
25
+ m === "light" ? "light" : "dark";
26
+ })();
27
+ </script>
28
+ <link rel="stylesheet" href="/theme.css" id="theme-css" />
29
+ <title>__DEN_TITLE__ · Tests</title>
8
30
  <style>
9
31
  *,
10
32
  *::before,
@@ -14,25 +36,12 @@
14
36
  padding: 0;
15
37
  }
16
38
 
17
- :root {
18
- --bg: #0d1117;
19
- --surface: #161b22;
20
- --border: #30363d;
21
- --text: #c9d1d9;
22
- --muted: #8b949e;
23
- --accent: #58a6ff;
24
- --green: #3fb950;
25
- --red: #f85149;
26
- --yellow: #d29922;
27
- --mono: "SF Mono", "Cascadia Code", "Fira Code", "Menlo", monospace;
28
- }
29
-
30
39
  html,
31
40
  body {
32
41
  height: 100%;
33
42
  background: var(--bg);
34
43
  color: var(--text);
35
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
44
+ font-family: var(--sans);
36
45
  font-size: 13px;
37
46
  }
38
47
  body {
@@ -42,75 +51,10 @@
42
51
  }
43
52
 
44
53
  /* ── Bar 1: top navbar ─────────────────────────────────────────────────── */
45
- .topbar {
46
- display: flex;
47
- align-items: center;
48
- gap: 10px;
49
- padding: 7px 14px;
50
- background: var(--surface);
51
- border-bottom: 1px solid var(--border);
52
- flex-shrink: 0;
53
- z-index: 10;
54
- }
55
- .topbar h1 {
56
- font-family: var(--mono);
57
- font-size: 14px;
58
- font-weight: 600;
59
- letter-spacing: -0.02em;
60
- margin-right: 4px;
61
- }
62
54
 
63
55
  /* ── Env badge (subbar selector pill) ────────────────────────────────── */
64
- .env-badge {
65
- display: inline-flex;
66
- align-items: center;
67
- gap: 5px;
68
- padding: 2px 8px 2px 6px;
69
- border-radius: 999px;
70
- background: var(--surface);
71
- border: 1px solid var(--border);
72
- font-size: 11px;
73
- font-weight: 500;
74
- color: var(--muted);
75
- }
76
- .env-badge::before {
77
- content: "env";
78
- font-size: 9px;
79
- font-weight: 600;
80
- letter-spacing: 0.05em;
81
- color: var(--muted);
82
- opacity: 0.6;
83
- text-transform: uppercase;
84
- }
85
- .env-badge select {
86
- background: transparent;
87
- border: none;
88
- outline: none;
89
- color: var(--text);
90
- font-size: 11px;
91
- font-weight: 600;
92
- cursor: pointer;
93
- padding: 0;
94
- -webkit-appearance: none;
95
- appearance: none;
96
- }
97
- .env-badge select option {
98
- background: var(--surface);
99
- }
100
56
 
101
57
  /* ── Topbar env pill ──────────────────────────────────────────────────── */
102
- #topbar-env-badge {
103
- display: none;
104
- align-items: center;
105
- padding: 2px 7px;
106
- border-radius: 999px;
107
- border: 1px solid var(--border);
108
- font-size: 10px;
109
- font-weight: 600;
110
- letter-spacing: 0.03em;
111
- color: var(--muted);
112
- white-space: nowrap;
113
- }
114
58
 
115
59
  /* ── Bar 2: sub-navbar ─────────────────────────────────────────────────── */
116
60
  .subbar {
@@ -137,20 +81,20 @@
137
81
  display: inline-block;
138
82
  }
139
83
  .dot.ok {
140
- background: var(--green);
84
+ background: var(--ok);
141
85
  }
142
86
  .dot.err {
143
- background: var(--red);
87
+ background: var(--bad);
144
88
  }
145
89
  .dot.running {
146
- background: var(--yellow);
90
+ background: var(--warn);
147
91
  animation: pulse 1s ease-in-out infinite;
148
92
  }
149
93
  .dot.passed {
150
- background: var(--green);
94
+ background: var(--ok);
151
95
  }
152
96
  .dot.failed {
153
- background: var(--red);
97
+ background: var(--bad);
154
98
  }
155
99
  @keyframes pulse {
156
100
  0%,
@@ -190,33 +134,9 @@
190
134
  color: var(--text);
191
135
  }
192
136
 
193
- nav {
194
- display: flex;
195
- gap: 2px;
196
- }
197
- nav a {
198
- color: var(--muted);
199
- text-decoration: none;
200
- padding: 3px 10px;
201
- border-radius: 4px;
202
- font-size: 12px;
203
- font-weight: 500;
204
- transition: color 0.1s;
205
- display: flex;
206
- align-items: center;
207
- gap: 5px;
208
- }
209
- nav a:hover {
210
- color: var(--text);
211
- background: var(--border);
212
- }
213
- nav a.active {
214
- color: var(--text);
215
- }
216
-
217
137
  .badge {
218
- background: var(--yellow);
219
- color: #0d1117;
138
+ background: var(--warn);
139
+ color: var(--bg);
220
140
  font-size: 10px;
221
141
  font-weight: 700;
222
142
  padding: 1px 5px;
@@ -275,7 +195,7 @@
275
195
  border-bottom: 1px solid var(--border);
276
196
  }
277
197
  .sec-table td a {
278
- color: var(--accent, #58a6ff);
198
+ color: var(--accent-text);
279
199
  }
280
200
  .sec-finding td {
281
201
  color: var(--muted);
@@ -284,20 +204,20 @@
284
204
  }
285
205
  .sec-hit.sec-high,
286
206
  span.sec-high {
287
- color: #f85149;
207
+ color: var(--bad);
288
208
  font-weight: 600;
289
209
  }
290
210
  .sec-hit.sec-medium,
291
211
  span.sec-medium {
292
- color: #d29922;
212
+ color: var(--warn);
293
213
  font-weight: 600;
294
214
  }
295
215
  .sec-hit.sec-low,
296
216
  span.sec-low {
297
- color: #8b949e;
217
+ color: var(--muted);
298
218
  }
299
219
  .sec-err {
300
- color: #f85149;
220
+ color: var(--bad);
301
221
  }
302
222
  .smoke-override {
303
223
  display: none;
@@ -380,7 +300,7 @@
380
300
  min-width: 180px;
381
301
  max-height: 260px;
382
302
  overflow-y: auto;
383
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
303
+ box-shadow: var(--shadow);
384
304
  }
385
305
  .module-picker-dropdown.open {
386
306
  display: block;
@@ -420,40 +340,11 @@
420
340
  }
421
341
 
422
342
  /* ── Health indicators ─────────────────────────────────────────────────── */
423
- .health {
424
- display: flex;
425
- align-items: center;
426
- gap: 10px;
427
- padding: 0 4px;
428
- }
429
- .health-item {
430
- display: flex;
431
- align-items: center;
432
- gap: 5px;
433
- font-size: 11px;
434
- color: var(--muted);
435
- }
436
- .health-group {
437
- display: flex;
438
- align-items: center;
439
- gap: 4px;
440
- font-size: 11px;
441
- color: var(--muted);
442
- }
443
- .health-group-label {
444
- color: var(--muted);
445
- margin-right: 1px;
446
- }
447
- .health-group-dots {
448
- display: flex;
449
- align-items: center;
450
- gap: 3px;
451
- }
452
343
 
453
344
  /* ── Buttons ───────────────────────────────────────────────────────────── */
454
345
  .run-btn {
455
- background: var(--accent);
456
- color: #0d1117;
346
+ background: var(--accent-fill);
347
+ color: var(--on-accent);
457
348
  border: none;
458
349
  padding: 5px 18px;
459
350
  border-radius: 6px;
@@ -584,7 +475,7 @@
584
475
  font-weight: 600;
585
476
  }
586
477
  .test-source-ws {
587
- color: var(--accent);
478
+ color: var(--accent-text);
588
479
  }
589
480
  .test-source-branch {
590
481
  color: var(--text);
@@ -626,8 +517,10 @@
626
517
  .tests-main.layout-top {
627
518
  flex-direction: column;
628
519
  }
520
+ /* The output is the shell's runs dock (R16): the controls take their
521
+ own height. */
629
522
  .tests-main.layout-top .left-panel {
630
- flex: 0 0 42%;
523
+ flex: 0 0 auto;
631
524
  border-bottom: 1px solid var(--border);
632
525
  }
633
526
  .tests-main.layout-top .report-pane {
@@ -652,36 +545,6 @@
652
545
  flex-wrap: wrap;
653
546
  }
654
547
 
655
- .output-pane {
656
- flex: 1;
657
- display: flex;
658
- flex-direction: column;
659
- min-height: 0;
660
- }
661
- #test-output {
662
- flex: 1;
663
- overflow-y: auto;
664
- padding: 10px 14px;
665
- font-family: var(--mono);
666
- font-size: 11.5px;
667
- line-height: 1.65;
668
- white-space: pre-wrap;
669
- word-break: break-all;
670
- min-height: 0;
671
- }
672
- #test-output .cmd {
673
- color: var(--accent);
674
- }
675
- #test-output .err {
676
- color: var(--red);
677
- }
678
- #test-output .done-ok {
679
- color: var(--green);
680
- }
681
- #test-output .done-fail {
682
- color: var(--red);
683
- }
684
-
685
548
  .divider {
686
549
  flex-shrink: 0;
687
550
  background: var(--border);
@@ -718,7 +581,7 @@
718
581
  flex: 1;
719
582
  border: none;
720
583
  width: 100%;
721
- background: #fff;
584
+ background: var(--bg);
722
585
  display: none;
723
586
  }
724
587
  #report-placeholder {
@@ -804,8 +667,8 @@
804
667
  width: 22px;
805
668
  height: 22px;
806
669
  border-radius: 6px;
807
- background: rgba(13, 17, 23, 0.75);
808
- border: 1px solid rgba(48, 54, 61, 0.8);
670
+ background: color-mix(in srgb, var(--bg) 75%, transparent);
671
+ border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
809
672
  color: var(--muted);
810
673
  font-size: 15px;
811
674
  line-height: 1;
@@ -834,16 +697,16 @@
834
697
  padding: 4px 8px;
835
698
  }
836
699
  .perf-ok {
837
- background: rgba(63, 185, 80, 0.15);
838
- color: var(--green);
700
+ background: color-mix(in srgb, var(--ok) 15%, transparent);
701
+ color: var(--ok);
839
702
  }
840
703
  .perf-fail {
841
- background: rgba(248, 81, 73, 0.15);
842
- color: var(--red);
704
+ background: color-mix(in srgb, var(--bad) 15%, transparent);
705
+ color: var(--bad);
843
706
  }
844
707
  .perf-section {
845
708
  padding: 12px 14px;
846
- border-top: 1px solid rgba(48, 54, 61, 0.5);
709
+ border-top: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
847
710
  }
848
711
  .perf-section:first-of-type {
849
712
  border-top: none;
@@ -855,10 +718,10 @@
855
718
  margin-top: 10px;
856
719
  }
857
720
  .perf-stat {
858
- border: 1px solid rgba(48, 54, 61, 0.7);
721
+ border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
859
722
  border-radius: 8px;
860
723
  padding: 10px;
861
- background: rgba(13, 17, 23, 0.55);
724
+ background: color-mix(in srgb, var(--bg) 55%, transparent);
862
725
  display: flex;
863
726
  flex-direction: column;
864
727
  gap: 6px;
@@ -875,7 +738,7 @@
875
738
  }
876
739
  .perf-table th,
877
740
  .perf-table td {
878
- border-top: 1px solid rgba(48, 54, 61, 0.7);
741
+ border-top: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
879
742
  padding: 8px 6px;
880
743
  text-align: left;
881
744
  vertical-align: top;
@@ -902,8 +765,8 @@
902
765
  margin: 0 8px 6px 0;
903
766
  padding: 3px 8px;
904
767
  border-radius: 999px;
905
- border: 1px solid rgba(48, 54, 61, 0.7);
906
- background: rgba(13, 17, 23, 0.55);
768
+ border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
769
+ background: color-mix(in srgb, var(--bg) 55%, transparent);
907
770
  font-size: 11px;
908
771
  white-space: nowrap;
909
772
  }
@@ -968,7 +831,7 @@
968
831
  }
969
832
  .cmd-btn:hover:not(:disabled) {
970
833
  border-color: var(--accent);
971
- color: var(--accent);
834
+ color: var(--accent-text);
972
835
  }
973
836
  .cmd-btn:disabled {
974
837
  opacity: 0.45;
@@ -978,12 +841,12 @@
978
841
  border-color: var(--muted);
979
842
  }
980
843
  .logs-btn:hover:not(:disabled) {
981
- border-color: var(--yellow) !important;
982
- color: var(--yellow) !important;
844
+ border-color: var(--warn) !important;
845
+ color: var(--warn) !important;
983
846
  }
984
847
  .logs-btn.streaming {
985
- border-color: var(--yellow);
986
- color: var(--yellow);
848
+ border-color: var(--warn);
849
+ color: var(--warn);
987
850
  }
988
851
 
989
852
  .empty-state {
@@ -993,40 +856,6 @@
993
856
  text-align: center;
994
857
  }
995
858
 
996
- /* exec output strip */
997
- .cmd-strip {
998
- flex: 1;
999
- display: flex;
1000
- flex-direction: column;
1001
- min-height: 0;
1002
- border-top: 1px solid var(--border);
1003
- }
1004
- .cmd-strip.hidden {
1005
- display: none;
1006
- }
1007
- #cmd-output {
1008
- flex: 1;
1009
- overflow-y: auto;
1010
- padding: 6px 14px;
1011
- font-family: var(--mono);
1012
- font-size: 11px;
1013
- line-height: 1.55;
1014
- white-space: pre-wrap;
1015
- word-break: break-all;
1016
- }
1017
- #cmd-output .cmd {
1018
- color: var(--accent);
1019
- }
1020
- #cmd-output .err {
1021
- color: var(--red);
1022
- }
1023
- #cmd-output .done-ok {
1024
- color: var(--green);
1025
- }
1026
- #cmd-output .done-fail {
1027
- color: var(--red);
1028
- }
1029
-
1030
859
  /* ── LOGS VIEW ─────────────────────────────────────────────────────────── */
1031
860
  #view-logs {
1032
861
  flex-direction: column;
@@ -1085,10 +914,10 @@
1085
914
  min-height: 0;
1086
915
  }
1087
916
  .log-output .log-cmd {
1088
- color: var(--accent);
917
+ color: var(--accent-text);
1089
918
  }
1090
919
  .log-output .log-err {
1091
- color: var(--red);
920
+ color: var(--bad);
1092
921
  }
1093
922
  .log-output .log-done {
1094
923
  color: var(--muted);
@@ -1101,13 +930,12 @@
1101
930
  align-items: center;
1102
931
  gap: 8px;
1103
932
  padding: 8px 12px;
1104
- background: color-mix(in srgb, var(--yellow, #d97706) 12%, var(--bg));
1105
- border-top: 1px solid
1106
- color-mix(in srgb, var(--yellow, #d97706) 40%, transparent);
933
+ background: color-mix(in srgb, var(--warn) 12%, var(--bg));
934
+ border-top: 1px solid color-mix(in srgb, var(--warn) 40%, transparent);
1107
935
  }
1108
936
  .aws-auth-msg {
1109
937
  font-size: 12px;
1110
- color: var(--yellow, #d97706);
938
+ color: var(--warn);
1111
939
  flex: 1;
1112
940
  }
1113
941
  .aws-auth-btn {
@@ -1115,8 +943,8 @@
1115
943
  padding: 3px 10px;
1116
944
  border-radius: 4px;
1117
945
  cursor: pointer;
1118
- background: var(--yellow, #d97706);
1119
- color: #000;
946
+ background: var(--warn);
947
+ color: var(--bg);
1120
948
  border: none;
1121
949
  font-weight: 600;
1122
950
  }
@@ -1131,7 +959,7 @@
1131
959
  font-family: var(--mono);
1132
960
  font-size: 11px;
1133
961
  line-height: 1.5;
1134
- background: var(--bg2);
962
+ background: var(--sunken);
1135
963
  border-radius: 4px;
1136
964
  white-space: pre-wrap;
1137
965
  max-height: 120px;
@@ -1166,24 +994,14 @@
1166
994
  </style>
1167
995
  </head>
1168
996
  <body>
1169
- <!-- ── Bar 1: __DEN_CLI__ + health + nav ──────────────────────────────────────── -->
1170
- <div class="topbar">
1171
- <img
1172
- src="/favicon.ico"
1173
- width="16"
1174
- height="16"
1175
- alt=""
1176
- style="flex-shrink: 0"
1177
- />
1178
- <h1>__DEN_CLI__</h1>
1179
- <button
1180
- class="v-pill"
1181
- id="version-pill"
1182
- onclick="openChangelog()"
1183
- title="View changelog"
1184
- >
1185
- <span class="v-pip" id="v-pip"></span><span class="v-txt">v…</span>
1186
- </button>
997
+ <!-- The shell: top bar and rail, the same on every page (shell.js). -->
998
+ <div id="den-shell" data-page="tests"></div>
999
+ <script src="/shell.js"></script>
1000
+
1001
+ <!-- The page's one main landmark: everything after the shell. -->
1002
+ <main id="den-content">
1003
+ <!-- ── Bar 2: context actions (the env picker is the shell's) ───────────── -->
1004
+ <div class="subbar">
1187
1005
  <a
1188
1006
  id="app-link"
1189
1007
  href="#"
@@ -1198,39 +1016,6 @@
1198
1016
  "
1199
1017
  >↗ open app</a
1200
1018
  >
1201
- <span id="topbar-env-badge"></span>
1202
- <span
1203
- id="ws-pill"
1204
- style="
1205
- font-size: 11px;
1206
- font-family: var(--mono);
1207
- padding: 2px 8px;
1208
- border-radius: 10px;
1209
- border: 1px solid #388bfd44;
1210
- color: #58a6ff;
1211
- background: #58a6ff10;
1212
- display: none;
1213
- "
1214
- ></span>
1215
- <div class="health" id="health" title="Service health"></div>
1216
- <div class="spacer"></div>
1217
- <nav>
1218
- <a href="/">Local Dev</a>
1219
- <a href="/services">Services</a>
1220
- <a href="/environments">Environments</a>
1221
- <a href="/release">Releases</a>
1222
- <a href="/logs">Logs <span class="badge" id="log-badge"></span></a>
1223
- <a href="/tests" id="nav-tests" class="active">Tests</a>
1224
- </nav>
1225
- </div>
1226
-
1227
- <!-- ── Bar 2: env selector (universal) + context actions (right) ──────────── -->
1228
- <div class="subbar">
1229
- <div class="env-badge">
1230
- <select id="env-sel" title="Environment">
1231
- <option value="local">local</option>
1232
- </select>
1233
- </div>
1234
1019
 
1235
1020
  <!-- Tests context: run status + layout toggle + run button -->
1236
1021
  <div class="bar2-slot active" id="bar2-tests">
@@ -1325,35 +1110,6 @@
1325
1110
  </div>
1326
1111
 
1327
1112
  <div class="test-sources-bar" id="test-sources-bar"></div>
1328
-
1329
- <div class="output-pane">
1330
- <div class="pane-bar">
1331
- <span>Output</span>
1332
- <button id="clear-test-output">Clear</button>
1333
- <button
1334
- id="copy-test-output"
1335
- title="Copy to clipboard"
1336
- style="padding: 2px 5px; line-height: 1"
1337
- >
1338
- <svg
1339
- width="13"
1340
- height="13"
1341
- viewBox="0 0 13 13"
1342
- fill="none"
1343
- stroke="currentColor"
1344
- stroke-width="1.3"
1345
- stroke-linecap="round"
1346
- stroke-linejoin="round"
1347
- >
1348
- <rect x="4" y="0.5" width="8.5" height="8.5" rx="1.5" />
1349
- <path
1350
- d="M8.5 9v2.5a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h2.5"
1351
- />
1352
- </svg>
1353
- </button>
1354
- </div>
1355
- <pre id="test-output"></pre>
1356
- </div>
1357
1113
  </div>
1358
1114
 
1359
1115
  <div class="divider" id="divider"></div>
@@ -1391,15 +1147,7 @@
1391
1147
  <div class="empty-state">Loading services&#8230;</div>
1392
1148
  </div>
1393
1149
 
1394
- <!-- Command output strip — shown when an exec command is running -->
1395
- <div class="cmd-strip hidden" id="cmd-strip">
1396
- <div class="pane-bar">
1397
- <span id="cmd-strip-title">Command Output</span>
1398
- <button id="stop-cmd" style="display: none">Stop</button>
1399
- <button id="clear-cmd-output">Clear</button>
1400
- </div>
1401
- <pre id="cmd-output"></pre>
1402
- </div>
1150
+ <!-- A command's output is a tab in the shell's runs dock (R16). -->
1403
1151
  </div>
1404
1152
 
1405
1153
  <script type="module">
@@ -1411,8 +1159,9 @@
1411
1159
  initLogBadge,
1412
1160
  storeOpenLog,
1413
1161
  initChangelog,
1414
- copyToClipboard,
1162
+ envReady,
1415
1163
  } from "/header.js";
1164
+ import { ownersLabel } from "/workspace-picker.js";
1416
1165
  import {
1417
1166
  buildReportPaneModel,
1418
1167
  getNextReportViewOnPillarChange,
@@ -1444,15 +1193,9 @@
1444
1193
  // ── Init ──────────────────────────────────────────────────────────────────
1445
1194
  async function init() {
1446
1195
  // Populate env selector
1447
- const envs = await fetch("/api/environments")
1448
- .then((r) => r.json())
1449
- .catch(() => ["local"]);
1196
+ // The shell's env picker (header.js), one for every page.
1197
+ await envReady();
1450
1198
  const envSel = document.getElementById("env-sel");
1451
- envSel.innerHTML = envs
1452
- .map((e) => `<option value="${e}">${e}</option>`)
1453
- .join("");
1454
- const savedEnv = localStorage.getItem("__DEN_CLI__-env");
1455
- if (savedEnv && envs.includes(savedEnv)) envSel.value = savedEnv;
1456
1199
  updateAppLink(envSel.value);
1457
1200
 
1458
1201
  // Restore pillar
@@ -1480,7 +1223,7 @@
1480
1223
  await Promise.all([
1481
1224
  loadModules(pillar),
1482
1225
  initHealth(),
1483
- initWorkspacePill("env-sel", { onSwitch: onWorkspaceSwitch }),
1226
+ initWorkspacePill(),
1484
1227
  applyDenConfig(),
1485
1228
  initChangelog(),
1486
1229
  ]);
@@ -1490,18 +1233,6 @@
1490
1233
  routeFromHash();
1491
1234
  }
1492
1235
 
1493
- // ── Workspace ─────────────────────────────────────────────────────────────
1494
- // Module discovery, test sources and the run itself all resolve repos from
1495
- // the FOCUSED workspace server-side (discoverRepos → getActiveWorkspace), so
1496
- // switching focus has to re-pull everything this page derives from it.
1497
- async function onWorkspaceSwitch() {
1498
- await loadModules(pillar);
1499
- refreshHealth(document.getElementById("env-sel").value, {
1500
- force: true,
1501
- });
1502
- if (currentView === "services") loadServices();
1503
- }
1504
-
1505
1236
  // ── Routing ───────────────────────────────────────────────────────────────
1506
1237
  window.addEventListener("hashchange", routeFromHash);
1507
1238
 
@@ -1530,7 +1261,6 @@
1530
1261
  // ── App link ─────────────────────────────────────────────────────────────
1531
1262
  async function updateAppLink(env) {
1532
1263
  const link = document.getElementById("app-link");
1533
- const badge = document.getElementById("topbar-env-badge");
1534
1264
  if (!link) return;
1535
1265
  try {
1536
1266
  const { url } = await fetch(`/api/app-url?env=${env}`).then((r) =>
@@ -1540,24 +1270,17 @@
1540
1270
  link.href = url;
1541
1271
  link.style.display = "";
1542
1272
  link.title = url;
1543
- if (badge) {
1544
- badge.textContent = env;
1545
- badge.style.display = "inline-flex";
1546
- }
1547
1273
  } else {
1548
1274
  link.style.display = "none";
1549
- if (badge) badge.style.display = "none";
1550
1275
  }
1551
1276
  } catch {
1552
1277
  link.style.display = "none";
1553
- if (badge) badge.style.display = "none";
1554
1278
  }
1555
1279
  }
1556
1280
 
1557
1281
  // ── Env ───────────────────────────────────────────────────────────────────
1558
1282
  document.getElementById("env-sel").addEventListener("change", () => {
1559
1283
  const env = document.getElementById("env-sel").value;
1560
- localStorage.setItem("__DEN_CLI__-env", env);
1561
1284
  updateAppLink(env);
1562
1285
 
1563
1286
  // Update health dots for the new env immediately
@@ -2090,6 +1813,12 @@
2090
1813
  setRunStatus("running", "running\u2026");
2091
1814
  document.getElementById("run-btn").disabled = true;
2092
1815
  resetReportPane();
1816
+ // The output is one tab in the shell's runs dock (R16), kept across
1817
+ // runs as the pane was; Clear there also clears the raw report.
1818
+ runs().start(TEST_RUN, `tests ${pillar}`, {
1819
+ doneLine: false,
1820
+ onClear: clearTestOutput,
1821
+ });
2093
1822
  testAppendLine(
2094
1823
  `\n\u25b6 ${pillar}${label ? " / " + label : ""} \u00b7 ${env}\n`,
2095
1824
  "cmd",
@@ -2115,6 +1844,7 @@
2115
1844
  ok ? "passed" : "failed",
2116
1845
  ok ? `passed (${dur})` : `failed (${dur})`,
2117
1846
  );
1847
+ runs().finish(TEST_RUN, { ok });
2118
1848
  document.getElementById("run-btn").disabled = false;
2119
1849
  testEs.close();
2120
1850
  testEs = null;
@@ -2141,6 +1871,7 @@
2141
1871
  };
2142
1872
  testEs.onerror = () => {
2143
1873
  testAppendLine("Connection closed", "err");
1874
+ runs().finish(TEST_RUN, { ok: false });
2144
1875
  setRunStatus("failed", "error");
2145
1876
  document.getElementById("run-btn").disabled = false;
2146
1877
  testEs?.close();
@@ -2148,12 +1879,12 @@
2148
1879
  };
2149
1880
  }
2150
1881
 
2151
- const _progressSpans = {};
1882
+ const TEST_RUN = "tests";
1883
+ const runs = () => window.denShell.runs;
2152
1884
  const _k6ProgressRe = /^(\S+)\s+[\[✓✗]/;
2153
1885
  const _k6RunningRe = /^running \(\d+m[\d.]+s\), \d+\/\d+ VUs,/;
2154
1886
 
2155
1887
  function testAppendLine(text, cls) {
2156
- const out = document.getElementById("test-output");
2157
1888
  const raw = document.getElementById("report-raw-output");
2158
1889
 
2159
1890
  // k6 "running (...)" status lines are redundant with the progress bar;
@@ -2163,60 +1894,24 @@
2163
1894
  return;
2164
1895
  }
2165
1896
 
2166
- // k6 progress/result lines: update a single DOM element per scenario
2167
- // so the UI shows a compact live-updating progress bar.
1897
+ // k6 progress/result lines: one line per scenario, updated in place
1898
+ // (a keyed line in the dock), so the UI shows a compact live-updating
1899
+ // progress bar.
2168
1900
  const pm = !cls && text.match(_k6ProgressRe);
2169
1901
  if (pm) {
2170
- const key = pm[1];
2171
- if (_progressSpans[key]) {
2172
- _progressSpans[key].textContent = text + "\n";
2173
- out.scrollTop = out.scrollHeight;
2174
- return;
2175
- }
2176
- const span = document.createElement("span");
2177
- span.className = "k6-progress";
2178
- span.textContent = text + "\n";
2179
- _progressSpans[key] = span;
2180
- out.appendChild(span);
2181
- out.scrollTop = out.scrollHeight;
1902
+ runs().append(TEST_RUN, text, "k6-progress", { key: pm[1] });
2182
1903
  return;
2183
1904
  }
2184
1905
 
2185
- const span = document.createElement("span");
2186
- if (cls) span.className = cls;
2187
- span.textContent = text + "\n";
2188
- out.appendChild(span);
2189
- out.scrollTop = out.scrollHeight;
1906
+ runs().append(TEST_RUN, text, cls);
2190
1907
  raw.textContent += text + "\n";
2191
1908
  raw.scrollTop = raw.scrollHeight;
2192
1909
  }
2193
1910
 
2194
- document
2195
- .getElementById("copy-test-output")
2196
- .addEventListener("click", () => {
2197
- const text = document.getElementById("test-output").innerText;
2198
- const btn = document.getElementById("copy-test-output");
2199
- const icon =
2200
- '<svg width="13" height="13" viewBox="0 0 13 13" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="0.5" width="8.5" height="8.5" rx="1.5"/><path d="M8.5 9v2.5a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h2.5"/></svg>';
2201
- const check =
2202
- '<svg width="13" height="13" viewBox="0 0 13 13" fill="none" stroke="var(--green)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 7l3 3 6-7"/></svg>';
2203
- copyToClipboard(text)
2204
- .then(() => {
2205
- btn.innerHTML = check;
2206
- setTimeout(() => {
2207
- btn.innerHTML = icon;
2208
- }, 1500);
2209
- })
2210
- .catch(() => {});
2211
- });
2212
-
2213
- document
2214
- .getElementById("clear-test-output")
2215
- .addEventListener("click", () => {
2216
- document.getElementById("test-output").innerHTML = "";
1911
+ /** The dock's Clear on the tests tab: the raw report output too. */
1912
+ function clearTestOutput() {
2217
1913
  document.getElementById("report-raw-output").textContent = "";
2218
- Object.keys(_progressSpans).forEach((k) => delete _progressSpans[k]);
2219
- });
1914
+ }
2220
1915
 
2221
1916
  function setRunStatus(state, text) {
2222
1917
  document.getElementById("run-dot").className = `dot ${state}`;
@@ -2449,7 +2144,7 @@
2449
2144
  <span class="dot" id="svc-dot-${s.repo}-${s.service}"></span>
2450
2145
  <div>
2451
2146
  <div class="service-name">${s.service}</div>
2452
- <div class="service-repo">${s.repo}</div>
2147
+ <div class="service-repo">${s.repo}${ownersLabel(s.inWorkspaces)}</div>
2453
2148
  </div>
2454
2149
  </div>
2455
2150
  <div class="cmd-row">
@@ -2477,12 +2172,23 @@
2477
2172
  if (el) el.className = `dot ${state}`;
2478
2173
  }
2479
2174
 
2480
- document.getElementById("stop-cmd").addEventListener("click", () => {
2481
- const sb = document.getElementById("stop-cmd");
2482
- const detachable = sb.dataset.detachable !== "0";
2175
+ // The running command's tab carries Stop (Detach for a daemon) in the
2176
+ // dock's bar while it runs.
2177
+ let execRun = null;
2178
+ let execDetachable = true;
2179
+ function stopAction() {
2180
+ return [
2181
+ {
2182
+ id: "stop",
2183
+ label: execDetachable ? "Detach" : "Stop",
2184
+ run: stopCmd,
2185
+ },
2186
+ ];
2187
+ }
2188
+ function stopCmd() {
2189
+ const detachable = execDetachable;
2483
2190
  execEs?.close();
2484
2191
  execEs = null;
2485
- sb.style.display = "none";
2486
2192
  document
2487
2193
  .querySelectorAll('.cmd-btn[data-type="exec"]')
2488
2194
  .forEach((b) => (b.disabled = false));
@@ -2491,7 +2197,8 @@
2491
2197
  detachable ? "--- Detached ---" : "--- Stopped ---",
2492
2198
  "err",
2493
2199
  );
2494
- });
2200
+ if (execRun) runs().set(execRun, { running: false, actions: [] });
2201
+ }
2495
2202
 
2496
2203
  // ── Service exec ──────────────────────────────────────────────────────────
2497
2204
  function runExec(repo, service, cmd) {
@@ -2505,15 +2212,19 @@
2505
2212
  document
2506
2213
  .querySelectorAll('.cmd-btn[data-type="exec"]')
2507
2214
  .forEach((b) => (b.disabled = true));
2508
- const stopBtn = document.getElementById("stop-cmd");
2509
- stopBtn.style.display = "";
2510
- stopBtn.textContent = "Stop";
2511
2215
  setSvcDot(repo, service, "running");
2512
2216
 
2513
- const strip = document.getElementById("cmd-strip");
2514
- strip.classList.remove("hidden");
2515
- document.getElementById("cmd-strip-title").textContent =
2516
- `${repo} / ${service} \u00b7 ${cmd}`;
2217
+ // The run's tab in the dock: one per service, the latest command.
2218
+ if (execRun) runs().set(execRun, { running: false, actions: [] });
2219
+ execDetachable = false;
2220
+ execRun = runs().start(
2221
+ `${repo}/${service}`,
2222
+ `${cmd} ${repo}/${service}`,
2223
+ {
2224
+ doneLine: false,
2225
+ actions: [{ id: "stop", label: "Stop", run: stopCmd }],
2226
+ },
2227
+ );
2517
2228
  document.getElementById("svc-status").textContent =
2518
2229
  `running ${service}:${cmd}\u2026`;
2519
2230
 
@@ -2528,9 +2239,8 @@
2528
2239
  const msg = JSON.parse(e.data);
2529
2240
  if (msg.type === "cmd") {
2530
2241
  cmdAppendLine(msg.text, "cmd");
2531
- const sb = document.getElementById("stop-cmd");
2532
- sb.textContent = msg.detached ? "Detach" : "Stop";
2533
- sb.dataset.detachable = msg.detached ? "1" : "0";
2242
+ execDetachable = !!msg.detached;
2243
+ runs().set(execRun, { actions: stopAction() });
2534
2244
  } else if (msg.type === "line") {
2535
2245
  cmdAppendLine(msg.text, "");
2536
2246
  } else if (msg.type === "error") {
@@ -2544,7 +2254,8 @@
2544
2254
  );
2545
2255
  setSvcDot(repo, service, ok ? "ok" : "err");
2546
2256
  document.getElementById("svc-status").textContent = "idle";
2547
- document.getElementById("stop-cmd").style.display = "none";
2257
+ runs().set(execRun, { actions: [] });
2258
+ runs().finish(execRun, { ok });
2548
2259
  document
2549
2260
  .querySelectorAll('.cmd-btn[data-type="exec"]')
2550
2261
  .forEach((b) => (b.disabled = false));
@@ -2556,7 +2267,8 @@
2556
2267
  cmdAppendLine("Connection closed", "err");
2557
2268
  setSvcDot(repo, service, "err");
2558
2269
  document.getElementById("svc-status").textContent = "idle";
2559
- document.getElementById("stop-cmd").style.display = "none";
2270
+ runs().set(execRun, { actions: [] });
2271
+ runs().finish(execRun, { ok: false });
2560
2272
  document
2561
2273
  .querySelectorAll('.cmd-btn[data-type="exec"]')
2562
2274
  .forEach((b) => (b.disabled = false));
@@ -2566,21 +2278,9 @@
2566
2278
  }
2567
2279
 
2568
2280
  function cmdAppendLine(text, cls) {
2569
- const out = document.getElementById("cmd-output");
2570
- const span = document.createElement("span");
2571
- if (cls) span.className = cls;
2572
- span.textContent = text + "\n";
2573
- out.appendChild(span);
2574
- out.scrollTop = out.scrollHeight;
2281
+ if (execRun) runs().append(execRun, text, cls);
2575
2282
  }
2576
2283
 
2577
- document
2578
- .getElementById("clear-cmd-output")
2579
- .addEventListener("click", () => {
2580
- document.getElementById("cmd-output").innerHTML = "";
2581
- document.getElementById("cmd-strip").classList.add("hidden");
2582
- });
2583
-
2584
2284
  // ── Helpers ───────────────────────────────────────────────────────────────
2585
2285
  function fmtDuration(ms) {
2586
2286
  if (ms < 1000) return `${ms}ms`;
@@ -2591,5 +2291,6 @@
2591
2291
 
2592
2292
  init();
2593
2293
  </script>
2294
+ </main>
2594
2295
  </body>
2595
2296
  </html>