@monster-orange/den 0.0.0-stage → 0.1.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 (231) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -2
  4. package/bin/den +61 -0
  5. package/docs/distributions.md +256 -0
  6. package/make/demo.mk +82 -0
  7. package/make/den.mk +80 -0
  8. package/make/journeys.mk +57 -0
  9. package/make/local_dev.mk +154 -0
  10. package/make/security.mk +59 -0
  11. package/make/test.mk +405 -0
  12. package/package.json +104 -4
  13. package/scripts/auth-configure.ts +415 -0
  14. package/scripts/heal-config.ts +92 -0
  15. package/scripts/jsonl-playwright-reporter.js +70 -0
  16. package/scripts/manifest-read.cjs +91 -0
  17. package/scripts/setup-1password.ts +570 -0
  18. package/scripts/setup-secrets.ts +149 -0
  19. package/scripts/view-results.js +156 -0
  20. package/service/install.sh +112 -0
  21. package/src/pillars/api/fixtures/index.ts +128 -0
  22. package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
  23. package/src/pillars/api/global-setup.ts +103 -0
  24. package/src/pillars/api/playwright.config.ts +105 -0
  25. package/src/pillars/api/schemas/example-api.json +110 -0
  26. package/src/pillars/api/tests/health/health.spec.ts +19 -0
  27. package/src/pillars/api/tests/health/manifest.yml +6 -0
  28. package/src/pillars/browser/fixtures/index.ts +232 -0
  29. package/src/pillars/browser/fixtures/package.json +10 -0
  30. package/src/pillars/browser/global-setup.ts +140 -0
  31. package/src/pillars/browser/global-teardown.ts +48 -0
  32. package/src/pillars/browser/playwright.config.ts +212 -0
  33. package/src/pillars/browser/setup.ts +219 -0
  34. package/src/pillars/journeys/app-config.ts +237 -0
  35. package/src/pillars/journeys/discover.ts +160 -0
  36. package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
  37. package/src/pillars/journeys/observability/events.ts +288 -0
  38. package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
  39. package/src/pillars/journeys/report/friction.ts +304 -0
  40. package/src/pillars/journeys/report/junit.ts +105 -0
  41. package/src/pillars/journeys/report/render.ts +273 -0
  42. package/src/pillars/journeys/report/results.ts +323 -0
  43. package/src/pillars/journeys/run.ts +673 -0
  44. package/src/pillars/journeys/runner.ts +1025 -0
  45. package/src/pillars/journeys/sandbox.ts +406 -0
  46. package/src/pillars/journeys/spec/loader.ts +152 -0
  47. package/src/pillars/journeys/spec/schema.ts +208 -0
  48. package/src/pillars/journeys/teardown.ts +98 -0
  49. package/src/pillars/journeys/tools/browser-session.ts +219 -0
  50. package/src/pillars/journeys/tools/browser-tool.ts +914 -0
  51. package/src/pillars/journeys/tools/http-tool.ts +564 -0
  52. package/src/pillars/journeys/tools/ledger.ts +278 -0
  53. package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
  54. package/src/pillars/journeys/verify/assertions.ts +502 -0
  55. package/src/pillars/journeys/verify/verdict.ts +71 -0
  56. package/src/pillars/perf/build-report.ts +36 -0
  57. package/src/pillars/perf/k6.config.js +112 -0
  58. package/src/pillars/perf/run.ts +345 -0
  59. package/src/pillars/perf/scenarios/load-profile.js +97 -0
  60. package/src/pillars/perf/staging.ts +73 -0
  61. package/src/pillars/security/discover.ts +108 -0
  62. package/src/pillars/security/findings.ts +151 -0
  63. package/src/pillars/security/plan.ts +355 -0
  64. package/src/pillars/security/run.ts +729 -0
  65. package/src/pillars/smoke/admin-probe.ts +68 -0
  66. package/src/pillars/smoke/base-url.ts +56 -0
  67. package/src/pillars/smoke/cleanup.ts +8 -0
  68. package/src/pillars/smoke/discover.ts +237 -0
  69. package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
  70. package/src/pillars/smoke/fixture-manager.ts +217 -0
  71. package/src/pillars/smoke/fixtures.ts +468 -0
  72. package/src/pillars/smoke/global-setup-all.ts +262 -0
  73. package/src/pillars/smoke/global-setup-global.ts +262 -0
  74. package/src/pillars/smoke/global-setup.ts +273 -0
  75. package/src/pillars/smoke/global-teardown-all.ts +8 -0
  76. package/src/pillars/smoke/global-teardown-global.ts +8 -0
  77. package/src/pillars/smoke/global-teardown.ts +139 -0
  78. package/src/pillars/smoke/playwright-all.config.ts +147 -0
  79. package/src/pillars/smoke/playwright-global.config.ts +141 -0
  80. package/src/pillars/smoke/playwright.config.ts +162 -0
  81. package/src/pillars/smoke/setup-env.ts +132 -0
  82. package/src/pillars/smoke/setup-global.ts +126 -0
  83. package/src/pillars/smoke/setup.ts +115 -0
  84. package/src/pillars/smoke/smoke-repo.ts +16 -0
  85. package/src/pillars/smoke/staging.ts +49 -0
  86. package/src/pillars/suites/run.ts +147 -0
  87. package/src/scripts/configure-aws-profiles.ts +114 -0
  88. package/src/scripts/register-repos.ts +307 -0
  89. package/src/scripts/setup-default-workspace.ts +203 -0
  90. package/src/shared/applications.ts +138 -0
  91. package/src/shared/auth/api-token.ts +416 -0
  92. package/src/shared/auth/auth0-helper.ts +284 -0
  93. package/src/shared/auth/headless-login.ts +265 -0
  94. package/src/shared/auth/jwt-helper.ts +186 -0
  95. package/src/shared/auth/sudo-vault.ts +69 -0
  96. package/src/shared/auth/test-accounts.ts +98 -0
  97. package/src/shared/auth/token-injection.ts +184 -0
  98. package/src/shared/auth/token-providers/auth0.ts +80 -0
  99. package/src/shared/auth/token-providers/index.ts +182 -0
  100. package/src/shared/auth/token-refresh.ts +273 -0
  101. package/src/shared/banner.ts +15 -0
  102. package/src/shared/brand.ts +79 -0
  103. package/src/shared/cli-tabs.ts +131 -0
  104. package/src/shared/constants.ts +128 -0
  105. package/src/shared/den-config.ts +566 -0
  106. package/src/shared/dev-metrics/emit.ts +354 -0
  107. package/src/shared/edge/config.ts +229 -0
  108. package/src/shared/edge/render.ts +221 -0
  109. package/src/shared/edge/runtime.ts +268 -0
  110. package/src/shared/edge/types.ts +62 -0
  111. package/src/shared/engine.ts +67 -0
  112. package/src/shared/environment/aws-profiles.ts +49 -0
  113. package/src/shared/environment/env-selector.ts +161 -0
  114. package/src/shared/environment/playwright-env.ts +82 -0
  115. package/src/shared/environment-manager/catalog.ts +77 -0
  116. package/src/shared/environment-manager/clone-guard.ts +163 -0
  117. package/src/shared/environment-manager/dag.ts +286 -0
  118. package/src/shared/environment-manager/default-workspace.ts +182 -0
  119. package/src/shared/environment-manager/derivation/alias.ts +90 -0
  120. package/src/shared/environment-manager/derivation/cache.ts +97 -0
  121. package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
  122. package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
  123. package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
  124. package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
  125. package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
  126. package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
  127. package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
  128. package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
  129. package/src/shared/environment-manager/derivation/scan.ts +160 -0
  130. package/src/shared/environment-manager/derivation/text.ts +40 -0
  131. package/src/shared/environment-manager/derivation/types.ts +69 -0
  132. package/src/shared/environment-manager/health.ts +589 -0
  133. package/src/shared/environment-manager/index.ts +6 -0
  134. package/src/shared/environment-manager/load-applications.ts +78 -0
  135. package/src/shared/environment-manager/log-tail.ts +212 -0
  136. package/src/shared/environment-manager/make-targets.ts +78 -0
  137. package/src/shared/environment-manager/opctl.ts +334 -0
  138. package/src/shared/environment-manager/readiness.ts +93 -0
  139. package/src/shared/environment-manager/register-local.ts +72 -0
  140. package/src/shared/environment-manager/secrets-check.ts +260 -0
  141. package/src/shared/environment-manager/service-env.ts +110 -0
  142. package/src/shared/environment-manager/service-runner.ts +1588 -0
  143. package/src/shared/environment-manager/up-plan.ts +302 -0
  144. package/src/shared/environment-manager/workspace-manager.ts +905 -0
  145. package/src/shared/environment-manager/workspaces.ts +860 -0
  146. package/src/shared/manifest/deps.ts +27 -0
  147. package/src/shared/manifest/migrate-manifest.ts +101 -0
  148. package/src/shared/manifest/read-manifest.ts +160 -0
  149. package/src/shared/manifest/schema.ts +299 -0
  150. package/src/shared/manifest/service-links.ts +93 -0
  151. package/src/shared/manifest/validate-manifest.ts +140 -0
  152. package/src/shared/manifest/workspace-manifests.ts +92 -0
  153. package/src/shared/open-url.ts +122 -0
  154. package/src/shared/org.ts +44 -0
  155. package/src/shared/paths.ts +178 -0
  156. package/src/shared/perf/optional-fields.ts +105 -0
  157. package/src/shared/perf/report-normalizer.ts +404 -0
  158. package/src/shared/perf/report-paths.ts +19 -0
  159. package/src/shared/perf/report-store.ts +110 -0
  160. package/src/shared/perf/report-types.ts +70 -0
  161. package/src/shared/perf/test-paths.ts +25 -0
  162. package/src/shared/process/log-rotation.ts +147 -0
  163. package/src/shared/process/spawn-streaming.ts +229 -0
  164. package/src/shared/product.ts +92 -0
  165. package/src/shared/profile.ts +141 -0
  166. package/src/shared/repo-kind.ts +32 -0
  167. package/src/shared/reporting/jsonl-reporter.ts +131 -0
  168. package/src/shared/reporting/slack-reporter.ts +340 -0
  169. package/src/shared/results/result-reader.ts +266 -0
  170. package/src/shared/results/result-writer.ts +168 -0
  171. package/src/shared/secrets/doppler.ts +126 -0
  172. package/src/shared/secrets/env.ts +48 -0
  173. package/src/shared/secrets/index.ts +197 -0
  174. package/src/shared/secrets/keys.ts +33 -0
  175. package/src/shared/secrets/onepassword.ts +75 -0
  176. package/src/shared/secrets/types.ts +52 -0
  177. package/src/shared/service-url.ts +119 -0
  178. package/src/shared/smoke/branch-resolver.ts +141 -0
  179. package/src/shared/smoke/git-remote.ts +70 -0
  180. package/src/shared/smoke/repo-cloner.ts +151 -0
  181. package/src/shared/stacks/config.ts +137 -0
  182. package/src/shared/stacks/manifest-commands.ts +88 -0
  183. package/src/shared/stacks/render.ts +253 -0
  184. package/src/shared/stacks/runtime.ts +334 -0
  185. package/src/shared/stacks/types.ts +53 -0
  186. package/src/shared/terminal/tabs.ts +212 -0
  187. package/src/shared/test-discovery/module-scanner.ts +569 -0
  188. package/src/shared/types/index.ts +678 -0
  189. package/src/shared/ui-marker.ts +19 -0
  190. package/src/ui/cli/applications-command.ts +116 -0
  191. package/src/ui/cli/config-command.ts +68 -0
  192. package/src/ui/cli/create-main.ts +9 -0
  193. package/src/ui/cli/create.ts +360 -0
  194. package/src/ui/cli/edge-command.ts +91 -0
  195. package/src/ui/cli/flag-parser.ts +246 -0
  196. package/src/ui/cli/graph-command.ts +524 -0
  197. package/src/ui/cli/health-display.ts +63 -0
  198. package/src/ui/cli/index.ts +3656 -0
  199. package/src/ui/cli/init.ts +855 -0
  200. package/src/ui/cli/invocation-target.ts +175 -0
  201. package/src/ui/cli/logs-command.ts +104 -0
  202. package/src/ui/cli/module-selection.ts +31 -0
  203. package/src/ui/cli/prompts.ts +767 -0
  204. package/src/ui/cli/results-viewer.ts +436 -0
  205. package/src/ui/cli/secrets-preflight.ts +91 -0
  206. package/src/ui/cli/stack-command.ts +144 -0
  207. package/src/ui/cli/stalled-setup-recovery.ts +129 -0
  208. package/src/ui/cli/test-runner.ts +1312 -0
  209. package/src/ui/cli/web-command.ts +1073 -0
  210. package/src/ui/server/changelog-parser.ts +94 -0
  211. package/src/ui/server/daemon-reaper.ts +129 -0
  212. package/src/ui/server/editor-manager.ts +301 -0
  213. package/src/ui/server/github-service.ts +223 -0
  214. package/src/ui/server/http-helpers.ts +98 -0
  215. package/src/ui/server/index.ts +3236 -0
  216. package/src/ui/server/log-history.ts +452 -0
  217. package/src/ui/server/perf-report.ts +42 -0
  218. package/src/ui/server/public/environments.html +494 -0
  219. package/src/ui/server/public/favicon.ico +0 -0
  220. package/src/ui/server/public/header.js +1371 -0
  221. package/src/ui/server/public/index.html +2595 -0
  222. package/src/ui/server/public/local-dev.html +7405 -0
  223. package/src/ui/server/public/logs.html +1489 -0
  224. package/src/ui/server/public/perf-report.js +359 -0
  225. package/src/ui/server/public/release.html +2111 -0
  226. package/src/ui/server/public/services.html +1098 -0
  227. package/src/ui/server/public/workspace-picker.js +69 -0
  228. package/src/ui/server/release-deploy-session.ts +246 -0
  229. package/src/ui/server/release-service.ts +1125 -0
  230. package/src/ui/server/sudo-store.ts +111 -0
  231. package/src/ui/server/workspace-routes.ts +2154 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,67 @@
1
+ # Changelog
2
+
3
+ All notable changes to Den are documented here. Each release lists the
4
+ config and manifest changes a distribution must make to move to it under
5
+ **Migrations**.
6
+
7
+ ## [0.1.0] - 2026-11-04
8
+
9
+ The first Den release: one engine for local development, tests and releases
10
+ across an organisation's repos, shipped as `@monster-orange/den` and used
11
+ through thin distributions that pin an exact version.
12
+
13
+ ### Added
14
+
15
+ - **`den create <name> [dir]`** (also `npx @monster-orange/den create`)
16
+ scaffolds a distribution: `den.yml`, a `package.json` pinning this exact
17
+ Den, a `bin/<cli>` shim, a Makefile including `make/den.mk`, an org config
18
+ with a brand, and empty `manifests/` and `tests/pillars/`.
19
+ `examples/distribution/` is its output for `acme`. See
20
+ `docs/distributions.md`.
21
+ - **Den runs as an installed package.** Den's own files resolve from the
22
+ engine root (`node_modules/@monster-orange/den`), the distribution's from
23
+ its root; scripts run with Den's tsx, never through `npx` or the current
24
+ directory. The package ships `src/` and runs it with tsx (a dependency
25
+ now); CI proves it with `scripts/dist-smoke.sh`.
26
+ - **Org > Application > Repo > Services.** Declared applications (config
27
+ `applications:` or a manifest's `application:`) group repos; the Local Dev
28
+ graph boxes each one inside its org's frame, next to the connected groups of
29
+ repos that are their own application.
30
+ - **Security scans keep a latest record.** Every run writes
31
+ `reports/security/<env>/latest-<mode>.json`; the Tests page shows it, with
32
+ findings marked new since the previous run.
33
+ - **Repo suites** (`tests:` in a manifest) run from the Tests page and CLI.
34
+ - **Edge opt-ins**: a project CA and route certificates are opt-in; with none
35
+ set, the rendered edge is unchanged.
36
+
37
+ - **launchd service from a distribution.** `service/install.sh` ships in the
38
+ package and runs from `DEN_ROOT`; `DEN_SERVICE_LABEL` and `DEN_SERVICE_LOGS`
39
+ keep an existing label and log folder; `print` shows the plist.
40
+
41
+ ### Migrations
42
+
43
+ From the engine this release was generalised from:
44
+
45
+ - Run Den from the package: a `bin/<cli>` shim that sets `DEN_ROOT` to the
46
+ distribution and execs `node_modules/@monster-orange/den/bin/den`, and a
47
+ Makefile that does `include node_modules/@monster-orange/den/make/den.mk`
48
+ (`den create` writes both; `docs/distributions.md` shows them).
49
+ - Add `den.yml` at the distribution root with `product: { name: <your name> }`.
50
+ Env vars, the config and manifest file names, the state, managed-clone and
51
+ workspace directories all derive from that name, so an existing install
52
+ keeps every on-disk and env name by naming itself as before.
53
+ - Workspace mode `<name>-managed` is now `managed`. Old values are still
54
+ read, and rewritten on the next save.
55
+ - The CLI command `use-<name>` is now `use-managed`.
56
+ - `github_org` has no default: set it in the org config.
57
+ - The shared local-infra repo is configuration: `graph.infra: { repo,
58
+ signals }`. Without it no infra edges are derived. The default-workspace
59
+ entry repo is `defaults.workspace.entry`.
60
+ - Perf suites come from the distribution (`tests/pillars/perf/<module>/test.js`)
61
+ or from repo manifests; Den ships none.
62
+ - Service and setup make targets are `<prefix>-up`, `-status`, … where the
63
+ prefix is `DEN_TARGET_PREFIX` (set before including `make/den.mk`; `den
64
+ create` sets it to the CLI name, so `make <name>-up` keeps working).
65
+ - `service_env` has no built-in entries: a shared-infra secrets file is
66
+ configured as `service_env: { <KEY>: "{repo:<infra repo>}/<path>" }`.
67
+ - The smoke and browser make targets have no default repo: pass `repo=<repo>`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Monster Orange
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,167 @@
1
- # Temporary Holding Version
1
+ # Den
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Den is a config-driven local development appliance. It starts the repos you
4
+ work on in dependency order, puts them behind a local HTTPS edge, gives you a
5
+ web UI and a CLI over all of it, and runs smoke, browser, API, journey,
6
+ security and performance suites against the environments you declare.
7
+
8
+ Den knows nothing about any particular organisation. Everything specific to
9
+ yours — product name, repos, environments, hosts, secrets backend, test
10
+ accounts — lives in **your distribution**, not in Den.
11
+
12
+ > **Status: 0.1.0**, the first release: `npm install @monster-orange/den`. Den is pre-1.0; each release lists the changes a distribution makes under **Migrations** in [CHANGELOG.md](CHANGELOG.md).
13
+
14
+ ## Make it your own
15
+
16
+ 1. **Create** a distribution named after your product:
17
+
18
+ ```sh
19
+ npx @monster-orange/den create acme # → ./acme
20
+ cd acme && npm install
21
+ ```
22
+
23
+ 2. **Name** it: `den.yml` holds `product: { name: acme }`, so the CLI is
24
+ `./bin/acme`, env vars are `ACME_*`, state lives in `~/.acme`. Pass
25
+ `--cli`, `--env-prefix` or `--title` to `create`, or edit `den.yml`.
26
+ 3. **Brand** it: `brand:` in `config/acme.yml` sets the name, tagline and
27
+ banner people see in the CLI and web UI.
28
+ 4. **Point it at your repos**: set `github_org` in `config/acme.yml`, then
29
+ `./bin/acme register ../shop-api` (or add a manifest per repo under
30
+ `manifests/`).
31
+ 5. **Run** it: `./bin/acme up`, `./bin/acme web`, `make test`.
32
+
33
+ Every field, the bin shim, the make fragment and upgrading are in
34
+ [docs/distributions.md](docs/distributions.md);
35
+ [examples/distribution/](examples/distribution) is what `create acme` writes.
36
+
37
+ ## Distributions
38
+
39
+ You run Den through a small repo of your own (a _distribution_):
40
+
41
+ ```
42
+ my-dev/
43
+ ├── den.yml # who this distribution is (below)
44
+ ├── package.json # pins an exact @monster-orange/den version
45
+ ├── config/<name>.yml # your org config
46
+ ├── config/environments/ # one <env>.json per environment
47
+ ├── manifests/ # manifests for repos that don't carry their own
48
+ ├── tests/pillars/<pillar>/ # your own suites (e.g. tests/pillars/perf/<module>/test.js)
49
+ ├── bin/<cli> # shim: DEN_ROOT=<this dir> exec node_modules/@monster-orange/den/bin/den
50
+ └── Makefile # include node_modules/@monster-orange/den/make/den.mk
51
+ ```
52
+
53
+ `den.yml` names the product. Every runtime name derives from it:
54
+
55
+ ```yaml
56
+ product:
57
+ name:
58
+ acme # CLI "acme", env vars ACME_*, config/acme.yml,
59
+ # manifests .acme.yml, state ~/.acme,
60
+ # managed clones .acme-services/, workspaces acme-workspaces/
61
+ # Override any of them: cli, env_prefix, config, manifest, state_dir,
62
+ # services_dir, workspaces_dir
63
+ ```
64
+
65
+ Den's own checkout is a distribution named `den`, so in this repo the CLI is
66
+ `./bin/den`, the config is `config/den.yml`, and env vars are `DEN_*`.
67
+
68
+ ## Repos opt in with a manifest
69
+
70
+ A repo joins by carrying a manifest at its root (`.<name>.yml`, e.g.
71
+ `.den.yml`) whose commands delegate to the repo's own targets. Den never
72
+ reaches inside a repo beyond that. When a repo has no manifest, the
73
+ distribution can hold one for it in `manifests/<repo>.<name>.yml`; the repo's
74
+ own manifest always wins.
75
+
76
+ ```yaml
77
+ # demo-api: a self-contained service. Nothing it calls, so it starts alone.
78
+ version: 1
79
+ name: demo-api
80
+ application: shop # the application this repo belongs to (den apps)
81
+ compose:
82
+ files: [docker-compose.yml]
83
+ routes:
84
+ api: api:8080 # https://api.demo-api.<edge domain>
85
+ services:
86
+ demo-api:
87
+ commands:
88
+ local:
89
+ setup: make setup
90
+ preflight: make test
91
+ # OWASP ZAP target for the security pillar (`make test-sec-api env=local`).
92
+ security:
93
+ api:
94
+ hosts:
95
+ local: ["https://api.demo-api.globex.localhost"]
96
+ ```
97
+
98
+ That is `examples/demo/demo-api/.den.yml`; the other demo repos are in
99
+ [`examples/demo/`](examples/demo/).
100
+
101
+ ## What it does
102
+
103
+ - **Workspaces.** A workspace is a set of repos. Each is either
104
+ _dev-managed_ (your checkout, your branch) or _managed_ (cloned by Den and
105
+ kept on a tracking branch).
106
+ - **Derived dependency graph.** Start and stop order comes from real signals in
107
+ each repo — SDK imports, `*_URL` env vars, IaC references, event rules,
108
+ readiness probes — tuned in config with `graph.aliases`, `graph.edges` and
109
+ an optional shared local-infra repo (`graph.infra`).
110
+ - **Local HTTPS edge.** One local proxy routes `https://<route>.<domain>` to
111
+ each service, with an optional project CA.
112
+ - **Test pillars.** `smoke`, `browser` (synthetic Playwright), `api`,
113
+ `journeys` (agent-driven), `perf` (k6), `security` (OWASP ZAP against hosts
114
+ you own) and `suites` (a repo's own commands).
115
+ - **Releases.** A release view over the repos and deploy workflows your config
116
+ lists.
117
+ - **Web UI and CLI** over the same state.
118
+
119
+ ## Strategies
120
+
121
+ Concerns that vary by org are strategies selected in config, never hard-wired:
122
+
123
+ | Concern | Built-in choices | Chosen in |
124
+ | ---------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
125
+ | Secrets backend | `env`, `doppler`, `1password` | `secrets.provider` in the org config; per engineer in `config/local/user.yml`; per shell `<PREFIX>_SECRETS_PROVIDER` |
126
+ | Test-user tokens | `local-jwt`, `cognito`, `auth0`, `none` | `auth.provider` in `config/environments/<env>.json` |
127
+
128
+ A new secrets backend implements `SecretsProvider`
129
+ (`src/shared/secrets/types.ts`); a new identity provider implements a token
130
+ provider in `src/shared/auth/token-providers/`.
131
+
132
+ ## CLI
133
+
134
+ ```bash
135
+ den up [repo] # start the repo's derived dependency closure (-x deps only, -n dry run)
136
+ den down [repo] # stop it cleanly
137
+ den status # service health for the active workspace
138
+ den apps # Org > Application > Repo > Services
139
+ den graph [repo] # the derived dependency graph
140
+ den logs [repo] # tail a repo's local logs
141
+ den use-local [repo] # use your checkout · use-managed [repo] to switch back
142
+ den workspace list # workspaces (activate · deactivate · use · create · delete)
143
+ den run # run test suites (interactive, or with options)
144
+ den web # the web UI
145
+ den --help # everything else
146
+ ```
147
+
148
+ In a distribution the command is that distribution's `cli` name.
149
+
150
+ ## Developing Den
151
+
152
+ ```bash
153
+ npm ci
154
+ npm run typecheck
155
+ npm run lint
156
+ npm test
157
+ DEN_ORG_NAMES_FILE=<patterns file> node scripts/org-name-gate.mjs
158
+ ```
159
+
160
+ CI runs the same gates. The org-name gate fails the build when any file names
161
+ an organisation, product, host or account; its patterns are kept out of this
162
+ repo (a CI secret) so the list itself is never published. See
163
+ [CONTRIBUTING.md](CONTRIBUTING.md).
164
+
165
+ ## License
166
+
167
+ [MIT](LICENSE)
package/bin/den ADDED
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Two roots (see src/shared/paths.ts):
5
+ # DEN_ENGINE_ROOT Den's own package: this script's parent directory, which
6
+ # in a distribution is node_modules/@monster-orange/den.
7
+ # DEN_ROOT the distribution: the directory holding den.yml. A
8
+ # distribution's bin/<cli> shim sets it; in Den's own
9
+ # checkout (where the engine dir holds den.yml) it is set
10
+ # here. Otherwise it is left unset and Den walks up from
11
+ # the entry script to find den.yml.
12
+ DEN_ENGINE_ROOT="$(cd "$(dirname "$(realpath "$0")")/.." && pwd)"
13
+ export DEN_ENGINE_ROOT
14
+ if [ -z "${DEN_ROOT:-}" ] && [ -f "$DEN_ENGINE_ROOT/den.yml" ]; then
15
+ DEN_ROOT="$DEN_ENGINE_ROOT"
16
+ export DEN_ROOT
17
+ fi
18
+ export DEN_INVOKE_CWD="$PWD"
19
+ export OP_ACCOUNT="${OP_ACCOUNT:-example.1password.com}"
20
+
21
+ # tsx: Den's own (a checkout) or hoisted into the distribution's node_modules.
22
+ find_tsx() {
23
+ local dir="$DEN_ENGINE_ROOT"
24
+ while :; do
25
+ if [ -x "$dir/node_modules/.bin/tsx" ]; then
26
+ printf '%s\n' "$dir/node_modules/.bin/tsx"
27
+ return 0
28
+ fi
29
+ [ "$dir" = "/" ] && return 1
30
+ dir="$(dirname "$dir")"
31
+ done
32
+ }
33
+
34
+ # `create` scaffolds a new distribution: it needs no den.yml, so it runs on
35
+ # its own entry point rather than the full CLI.
36
+ entry="src/ui/cli/index.ts"
37
+ built="dist/src/ui/cli/index.js"
38
+ if [ "${1:-}" = "create" ]; then
39
+ shift
40
+ entry="src/ui/cli/create-main.ts"
41
+ built="dist/src/ui/cli/create-main.js"
42
+ fi
43
+
44
+ # Escape hatch: force the compiled build (e.g. to test the prod artifact).
45
+ if [ "${DEN_USE_DIST:-}" = "1" ] && [ -f "$DEN_ENGINE_ROOT/$built" ]; then
46
+ exec node "$DEN_ENGINE_ROOT/$built" "$@"
47
+ fi
48
+
49
+ # Prefer live TypeScript source (the package ships src/ and runs it with tsx).
50
+ # A built dist/ is gitignored and only refreshed by `npm run build`, so
51
+ # preferring it would silently shadow local source edits with a stale build.
52
+ if tsx="$(find_tsx)"; then
53
+ exec "$tsx" "$DEN_ENGINE_ROOT/$entry" "$@"
54
+ fi
55
+
56
+ if [ -f "$DEN_ENGINE_ROOT/$built" ]; then
57
+ exec node "$DEN_ENGINE_ROOT/$built" "$@"
58
+ fi
59
+
60
+ echo "den: no runnable entrypoint: run 'npm install'" >&2
61
+ exit 1
@@ -0,0 +1,256 @@
1
+ # Distributions
2
+
3
+ Den is one engine, `@monster-orange/den`. You run it through a small repo of
4
+ your own, a **distribution**, that installs Den as a package and holds
5
+ everything that makes it yours: its name, brand, config, manifests and tests.
6
+ Den's own checkout is itself a distribution, named `den`.
7
+
8
+ - [Create one](#create-one)
9
+ - [Layout](#layout)
10
+ - [den.yml: the product profile](#denyml-the-product-profile)
11
+ - [Brand](#brand)
12
+ - [The bin shim](#the-bin-shim)
13
+ - [make: den.mk](#make-denmk)
14
+ - [Two roots](#two-roots)
15
+ - [Pinning and upgrading](#pinning-and-upgrading)
16
+ - [Developing Den against a distribution](#developing-den-against-a-distribution)
17
+
18
+ ## Create one
19
+
20
+ ```sh
21
+ npx @monster-orange/den create acme # writes ./acme
22
+ npx @monster-orange/den create acme ~/src/acme-dev --title "Acme Dev"
23
+ ```
24
+
25
+ From a Den checkout, `./bin/den create acme ../acme` does the same.
26
+
27
+ | Option | Default | What |
28
+ | ----------------------- | ------------------- | ------------------------------------- |
29
+ | `<name>` | (required) | Product name: `[a-z][a-z0-9-]*` |
30
+ | `[dir]` | `./<name>` | A new or empty directory |
31
+ | `--title <title>` | `<Name>` Title Case | Brand name shown to people |
32
+ | `--cli <cli>` | `<name>` | The command people type |
33
+ | `--env-prefix <PREFIX>` | `<NAME>` | Env-var prefix (`ACME` → `ACME_HOME`) |
34
+
35
+ `create` refuses a directory that is not empty and never overwrites a file.
36
+ [`examples/distribution/`](../examples/distribution) is exactly the output of
37
+ `den create acme`; a unit test regenerates it, so the two never drift
38
+ (`npm run example:regen` after changing the scaffolder).
39
+
40
+ Then:
41
+
42
+ ```sh
43
+ cd acme
44
+ npm install
45
+ ./bin/acme --help
46
+ ./bin/acme web # the web UI, branded "Acme"
47
+ make help
48
+ ```
49
+
50
+ ## Layout
51
+
52
+ | Path | What |
53
+ | ------------------------- | ------------------------------------------------------------ |
54
+ | `den.yml` | The product profile ([below](#denyml-the-product-profile)) |
55
+ | `package.json` | Pins an exact `@monster-orange/den` version |
56
+ | `bin/<cli>` | The shim people run ([below](#the-bin-shim)) |
57
+ | `Makefile` | Includes Den's targets ([below](#make-denmk)) |
58
+ | `config/<name>.yml` | Org config: brand, `github_org`, `secrets`, `edge`, … |
59
+ | `config/environments/` | One `<env>.json` per environment (base URLs, auth, timeouts) |
60
+ | `config/local/` | Per-engineer overrides (`user.yml`); gitignored |
61
+ | `manifests/` | Manifests for repos that do not carry their own |
62
+ | `tests/pillars/<pillar>/` | Your own suites, e.g. `tests/pillars/perf/<module>/test.js` |
63
+ | `reports/`, `results/` | Test output; gitignored |
64
+
65
+ ## den.yml: the product profile
66
+
67
+ Every runtime name derives from `product.name`. Set only what you want to
68
+ differ.
69
+
70
+ ```yaml
71
+ product:
72
+ name: acme
73
+ # cli: acme
74
+ # env_prefix: ACME
75
+ # config: config/acme.yml
76
+ # manifest: .acme.yml
77
+ # state_dir: ~/.acme
78
+ # services_dir: .acme-services
79
+ # workspaces_dir: acme-workspaces
80
+ ```
81
+
82
+ | Field | Default | What it names |
83
+ | ---------------- | -------------------- | ----------------------------------------------------------------- |
84
+ | `name` | `den` | The product; `[a-z][a-z0-9-]*`. Everything below derives from it |
85
+ | `cli` | `<name>` | The command shown in help, hints and the web UI |
86
+ | `env_prefix` | `<NAME>` (`-` → `_`) | Env vars: `<PREFIX>_ROOT`, `<PREFIX>_HOME`, `<PREFIX>_INVOKE_CWD` |
87
+ | `config` | `config/<name>.yml` | The org config, relative to the distribution root |
88
+ | `manifest` | `.<name>.yml` | The manifest file a repo carries; the `.json` twin is also read |
89
+ | `state_dir` | `~/.<name>` | Per-user state: web server, daemons, logs, history |
90
+ | `services_dir` | `.<name>-services` | Managed clones, relative to the distribution root |
91
+ | `workspaces_dir` | `<name>-workspaces` | Workspace checkouts, relative to the distribution root |
92
+
93
+ Env vars, with `ACME` as the prefix:
94
+
95
+ | Variable | What |
96
+ | ----------------- | ------------------------------------------------------------------ |
97
+ | `ACME_ROOT` | The distribution root, if Den cannot find it from the entry script |
98
+ | `ACME_HOME` | Overrides `state_dir` (two instances on one machine stay apart) |
99
+ | `DEN_ROOT` | The same as `<PREFIX>_ROOT`; the shim sets it |
100
+ | `DEN_ENGINE_ROOT` | Den's package directory; `bin/den` sets it |
101
+ | `DEN_PROFILE` | Read another profile file instead of `<root>/den.yml` (tests) |
102
+
103
+ ## Brand
104
+
105
+ What people see: the CLI banner and help header, web page titles and the
106
+ edge's web route. Read from `brand:` in the org config; every field is
107
+ optional.
108
+
109
+ ```yaml
110
+ brand:
111
+ name: Acme # default: the product name, first letter upper-cased
112
+ tagline: Acme local dev and E2E harness # default: "Local dev and E2E harness"
113
+ banner: # optional ASCII-art lines; default: the name spelled out
114
+ - " ┌───────────┐"
115
+ - " │ A C M E │"
116
+ - " └───────────┘"
117
+ ```
118
+
119
+ | Field | Used for |
120
+ | --------- | ---------------------------------------------------------------------------- |
121
+ | `name` | Web page `<title>`s, the banner fallback, the web route on the edge (`acme`) |
122
+ | `tagline` | The line under the banner |
123
+ | `banner` | The CLI and web server startup banner |
124
+
125
+ Runtime names (env vars, files, directories) always come from `den.yml`,
126
+ never from the brand.
127
+
128
+ ## The bin shim
129
+
130
+ `bin/<cli>` is three lines of sh: it names the distribution root and runs
131
+ Den from `node_modules`.
132
+
133
+ ```sh
134
+ #!/bin/sh
135
+ DEN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
136
+ export DEN_ROOT; exec "$DEN_ROOT/node_modules/@monster-orange/den/bin/den" "$@"
137
+ ```
138
+
139
+ `package.json` also lists it under `bin`, so `npm link` (or a global
140
+ install of the distribution) puts `acme` on your PATH.
141
+
142
+ Den's own `bin/den` sets `DEN_ENGINE_ROOT` to its own package directory and
143
+ sets `DEN_ROOT` only when it is unset and the engine directory holds a
144
+ `den.yml` (Den's own checkout). An installed Den ships no `den.yml`, so the
145
+ engine directory is never taken for a distribution. `DEN_USE_DIST=1` still
146
+ runs a compiled `dist/` build when one exists.
147
+
148
+ ## make: den.mk
149
+
150
+ A distribution's `Makefile` includes Den's targets from the package:
151
+
152
+ ```make
153
+ DEN_TARGET_PREFIX := acme
154
+ include node_modules/@monster-orange/den/make/den.mk
155
+ ```
156
+
157
+ `DEN_TARGET_PREFIX` names the service and setup targets: `make acme-up`,
158
+ `acme-down`, `acme-status`, `acme-logs`, `acme-graph`, `acme-workspace`,
159
+ `acme-setup` and the rest (default `den`). `den create` sets it to the CLI
160
+ name.
161
+
162
+ `den.mk` defines:
163
+
164
+ | Variable | What |
165
+ | ---------------- | --------------------------------------------------------------------- |
166
+ | `DEN_ENGINE` | Den's package directory (the directory above `den.mk`) |
167
+ | `DEN_TSX` | The tsx binary: Den's own, or the one npm hoisted into `node_modules` |
168
+ | `DEN_PLAYWRIGHT` | The playwright binary, found the same way |
169
+ | `DEN_BIN` | `$(DEN_ENGINE)/bin/den` |
170
+ | `DEN_ROOT` | Exported: the directory make runs in (`$(CURDIR)`) |
171
+
172
+ and includes `make/test.mk`, `local_dev.mk`, `journeys.mk`, `security.mk` and
173
+ `demo.mk`: `make test`, `make web`, `make <prefix>-up`, `make test-syn-all
174
+ repo=<repo>`, `make test-smk-all repo=<repo>`, `make test-perf
175
+ module=<module>`, `make test-jrn-all`, `make test-sec-all` and the rest.
176
+ Recipes run from the distribution root (reports and staged tests land there)
177
+ and reach Den's files through `$(DEN_ENGINE)`. `make help` lists the main
178
+ targets; define your own `help` by setting `DEN_OWN_HELP := 1` before the
179
+ include. Add your own targets below it.
180
+
181
+ ## Two roots
182
+
183
+ Den resolves two directories (`src/shared/paths.ts`, `src/shared/engine.ts`):
184
+
185
+ - the **engine root** (`getEngineRoot()`, `enginePath()`): Den's own files,
186
+ `src/`, `make/`, `scripts/`; `node_modules/@monster-orange/den` in a
187
+ distribution;
188
+ - the **distribution root** (`getDistRoot()`, `distPath()`): `den.yml`,
189
+ config, manifests, `tests/pillars/`, reports, results, staged tests.
190
+
191
+ Den's scripts run with the engine's tsx (`tsxCommand()`), never through `npx`
192
+ or a path relative to the current directory. In Den's own checkout both roots
193
+ are the same directory.
194
+
195
+ ## Running the web UI as a service (macOS)
196
+
197
+ Den ships a launchd installer that runs the web UI at login and restarts it if
198
+ it exits. From the distribution root:
199
+
200
+ ```bash
201
+ DEN_ROOT="$PWD" DEN_SERVICE_LABEL=local.acme.web \
202
+ node_modules/@monster-orange/den/service/install.sh # install or refresh
203
+ node_modules/@monster-orange/den/service/install.sh status # with the same env
204
+ node_modules/@monster-orange/den/service/install.sh uninstall
205
+ node_modules/@monster-orange/den/service/install.sh print # the plist only
206
+ ```
207
+
208
+ | Variable | Default |
209
+ | ------------------- | ---------------------------------------- |
210
+ | `DEN_ROOT` | required in a distribution |
211
+ | `DEN_SERVICE_LABEL` | `local.<distribution dir name>.web` |
212
+ | `DEN_SERVICE_LOGS` | `~/Library/Logs/<distribution dir name>` |
213
+
214
+ The service runs the engine's server with the `node` on your `PATH` at install
215
+ time, from the distribution root; re-run `install` after changing node versions
216
+ or upgrading Den. Keep the label stable: it is how launchd knows the service.
217
+
218
+ ## Pinning and upgrading
219
+
220
+ `package.json` pins an exact Den version (`create` writes the version that
221
+ ran it). Den is pre-1.0: read the **Migrations** section of
222
+ [CHANGELOG.md](../CHANGELOG.md) for every version between yours and the
223
+ target, make the config and manifest changes it lists, then:
224
+
225
+ ```sh
226
+ npm install @monster-orange/den@<version> --save-exact
227
+ ./bin/acme --version
228
+ ```
229
+
230
+ Installing from a tarball (`npm pack` of a Den checkout, before a version is
231
+ on npm): every tarball Den hands out has its own prerelease number
232
+ (`0.1.0-dev.1`, `-dev.2`, …). npm caches a tarball by version and
233
+ integrity, so a repacked tarball with an unchanged version reinstalls the
234
+ old copy; if you ever repack one yourself without bumping, remove
235
+ `node_modules/@monster-orange/den` and its `package-lock.json` entry first.
236
+
237
+ ## Developing Den against a distribution
238
+
239
+ To try a Den change in a distribution, install your checkout instead of the
240
+ published package:
241
+
242
+ ```sh
243
+ cd acme
244
+ npm install ../den # node_modules/@monster-orange/den → your checkout
245
+ ./bin/acme status
246
+ ```
247
+
248
+ npm links the directory, so edits to Den's `src/` take effect on the next
249
+ run (Den runs its TypeScript with tsx; there is no build step). Den's
250
+ checkout has its own `den.yml`, but the shim's `DEN_ROOT` wins, so the CLI
251
+ still runs as the distribution. Put the pinned version back with `npm
252
+ install @monster-orange/den@<version> --save-exact` before committing.
253
+
254
+ To check a change the way CI does, run `scripts/dist-smoke.sh` in Den's
255
+ checkout: it packs Den, scaffolds `acme` from the tarball, installs the
256
+ tarball into it, and checks the CLI, make targets and web server.
package/make/demo.mk ADDED
@@ -0,0 +1,82 @@
1
+ # make/demo.mk — end-to-end proof of the repo flow on three demo repos
2
+ #
3
+ # examples/demo/demo-api standalone API
4
+ # examples/demo/demo-web calls demo-api (.den.yml compose.uses)
5
+ # examples/demo/demo-notes unrelated, standalone
6
+ #
7
+ # Each repo carries its own Makefile (run it alone) and .den.yml (how
8
+ # den runs it: compose behind the HTTPS edge, no host ports).
9
+ #
10
+ # make demo register + up + check + restart + down, then clean up
11
+ # make demo-register workspace "demo" with the three repos
12
+ # make demo-apps den apps: "shop" (declared by the repos) + demo-notes (its own)
13
+ # make demo-up den up -A shop (demo-api first), then demo-notes
14
+ # make demo-check HTTPS through the edge (retries ~10s while the edge
15
+ # picks up new routes); demo-web shows demo-api's answer
16
+ # make demo-status den status
17
+ # make demo-restart den restart -A shop
18
+ # make demo-down den down, every demo repo
19
+ # make demo-clean down, drop the demo workspace + catalog entries, refocus
20
+
21
+ .PHONY: demo demo-apps demo-register demo-up demo-check demo-status demo-graph demo-restart demo-down demo-clean
22
+
23
+ DEMO_WS := demo
24
+ DEMO_REPOS := demo-api demo-web demo-notes
25
+ DEMO_DIR := $(DEN_ENGINE)/examples/demo
26
+ DEMO_PREV := $(HOME)/.den/demo-prev-focus
27
+ DEMO_DOMAIN = $(shell $(DEN_TSX) -e 'import("$(DEN_ENGINE)/src/shared/edge/config.ts").then(m=>console.log(m.readEdgeConfig().domain))' 2>/dev/null)
28
+ DEN_CLI := $(DEN_BIN)
29
+
30
+ # Steps run in sequence (a prerequisite list would run demo-check only once).
31
+ demo:
32
+ @for t in demo-register demo-apps demo-graph demo-up demo-check demo-restart demo-check demo-status demo-clean; do \
33
+ echo "\n── make $$t"; $(MAKE) --no-print-directory $$t || { $(MAKE) --no-print-directory demo-clean; exit 1; }; \
34
+ done
35
+ @echo "\n✓ demo passed"
36
+
37
+ demo-register:
38
+ @mkdir -p $(dir $(DEMO_PREV))
39
+ @[ -f $(DEMO_PREV) ] || $(DEN_TSX) -e 'import("$(DEN_ENGINE)/src/shared/environment-manager/workspaces.ts").then(m=>console.log(m.getFocusedWorkspaceName()??"default"))' > $(DEMO_PREV)
40
+ @$(DEN_CLI) workspace create $(DEMO_WS) >/dev/null
41
+ @$(DEN_CLI) workspace use $(DEMO_WS) >/dev/null
42
+ @for r in $(DEMO_REPOS); do $(DEN_CLI) register $(DEMO_DIR)/$$r; done
43
+ @$(DEN_CLI) workspace list
44
+
45
+ demo-apps:
46
+ @$(DEN_CLI) apps
47
+
48
+ demo-graph:
49
+ @$(DEN_CLI) up -A shop --dry-run
50
+
51
+ demo-up:
52
+ @$(DEN_CLI) up -A shop
53
+ @$(DEN_CLI) up demo-notes
54
+
55
+ demo-check:
56
+ @set -e; for u in api.demo-api app.demo-web app.demo-notes; do \
57
+ for i in 1 2 3 4 5 6 7 8 9 10; do \
58
+ code=$$(curl -sk -o /dev/null -w '%{http_code}' https://$$u.$(DEMO_DOMAIN)/healthz); \
59
+ [ "$$code" = 200 ] && break; sleep 1; \
60
+ done; \
61
+ echo "$$code https://$$u.$(DEMO_DOMAIN)/healthz"; [ "$$code" = 200 ] || exit 1; \
62
+ done
63
+ @curl -sk https://app.demo-web.$(DEMO_DOMAIN)/ | grep -q '"service": "demo-api"' \
64
+ && echo "✓ demo-web reached demo-api over the link network" \
65
+ || { echo "✗ demo-web did not reach demo-api"; exit 1; }
66
+
67
+ demo-status:
68
+ @$(DEN_CLI) status
69
+
70
+ demo-restart:
71
+ @$(DEN_CLI) restart -A shop
72
+
73
+ demo-down:
74
+ @$(DEN_CLI) down -A shop
75
+ @$(DEN_CLI) down demo-notes
76
+
77
+ demo-clean: demo-down
78
+ @$(DEN_CLI) workspace use $$(cat $(DEMO_PREV) 2>/dev/null || echo default) >/dev/null
79
+ @$(DEN_CLI) workspace delete $(DEMO_WS) --yes >/dev/null
80
+ @$(DEN_TSX) -e 'import("$(DEN_ENGINE)/src/shared/environment-manager/catalog.ts").then(m=>{for(const r of process.argv.slice(1))m.removeCatalogEntry(r)})' $(DEMO_REPOS)
81
+ @echo "✓ demo workspace removed; focus back on $$(cat $(DEMO_PREV) 2>/dev/null || echo default)"
82
+ @rm -f $(DEMO_PREV)