@monster-orange/den 0.4.5 → 0.4.6

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,80 @@ All notable changes to Den are documented here. Each release lists the
4
4
  config and manifest changes a distribution must make to move to it under
5
5
  **Migrations**.
6
6
 
7
+ ## [0.4.6] - 2026-11-07
8
+
9
+ Den in five minutes, set up by your own agent (ON-2, with ON-4's dry run).
10
+ A developer pastes one line into their agent, and every command the agent
11
+ runs answers in JSON, never prompts, and is safe to re-run.
12
+
13
+ ### Added
14
+
15
+ - **Set up by your agent.** `den --help` and the README print the line to
16
+ paste: "Set up Den for me by following
17
+ https://raw.githubusercontent.com/Monster-Orange/den/main/skills/install-den/SKILL.md". The install-den skill it points
18
+ at ships in the package as `skills/install-den/SKILL.md`: plain Markdown
19
+ with Claude skill front matter, at most three questions, no secrets in
20
+ chat, no edits to the person's repos, nothing sent off the machine.
21
+ - **`<cli> repo add <path|git-url>`** adds a repo with no prompts and never
22
+ writes into it. A git URL is cloned where Den keeps managed clones. A repo
23
+ with no manifest (or `--sidecar`) gets a side-car manifest in the
24
+ distribution's `manifests/`, detected from its Makefile `up` target, its
25
+ compose file or its `package.json` `dev`/`start` scripts (and `test`), and
26
+ validated before it is written. When detection can't decide, nothing is
27
+ written and the answer asks, with the choices; a re-run passes
28
+ `--run "<cmd>"`. `--health <url>` adds a health check. The workspace is
29
+ `-w`, else the focused one, else a new `default`. The same repo again
30
+ answers `existed: true`.
31
+ - **`--json` for setup.** `create --yes --json` (a directory that already
32
+ holds the distribution is `existed: true`), `up --json` (each service's
33
+ result and health), `doctor --json` and `mcp connect --json`. Stdout is
34
+ one JSON object, the log goes to stderr, and a failure carries a `fix`
35
+ the agent can run or an `ask` for the person.
36
+ - **`doctor --json`'s onboarding checks:** Node 22.13 or later, git, Docker
37
+ (only when a registered repo needs it), the web server and its URL, and
38
+ Den's MCP through it, each `ok`, `warn` or `fail`. The human `doctor` is
39
+ unchanged.
40
+ - **`mcp connect --runtime claude|codex|gemini|grok`** names the agent by
41
+ its runtime. Grok CLI is a new client: Den writes its own
42
+ `[mcp_servers.<cli>]` table in `~/.grok/config.toml` (or the project's
43
+ `.grok/config.toml`), as xAI documents. A config file that already has the
44
+ entry is no longer rewritten.
45
+ - **`create` picks a free web port.** When 7777 is in use on this machine,
46
+ `create` picks the next free port above it (never 7778, 7791 or 7799),
47
+ writes it to the new distribution's `config/local/user.yml` and says so,
48
+ in JSON as `port` and `portPicked`. `--port <n>` sets it outright. The
49
+ check connects and binds only; it sends nothing to the port's holder.
50
+ `DEN_WEB_PORT_START` moves the starting port.
51
+ - **`web up --json`** answers `{ ok, url, port }` once the server answers,
52
+ and `doctor --json`'s web check carries the `port`.
53
+
54
+ ### Changed
55
+
56
+ - `create` into a directory that already holds the same distribution says
57
+ so and exits 0, instead of refusing a non-empty directory.
58
+
59
+ ### Fixed
60
+
61
+ - **An opted-in CLI sends its metrics.** The CLI records each run as it
62
+ exits, and a send started then never ran. With `metrics.share: true`, the
63
+ CLI now hands the event to a small detached sender that gives up after 2
64
+ seconds. The web server sends as before. Not opted in, nothing is sent.
65
+ - **Linked devices read plainly.** A device approved with a code says
66
+ "approved with a code", not "signed in with approved with a code".
67
+ - **An agent's refusal names the admin password.** It said "the lock",
68
+ which 0.4.4 removed.
69
+
70
+ ### Metric
71
+
72
+ - Time from the paste to "service running, MCP connected" (R11): not
73
+ measured before; ON-4's dry run (create, repo add, up, web up, doctor,
74
+ mcp connect, each run twice) now records it, about 16 s locally. Target:
75
+ 5 minutes or less.
76
+
77
+ ### Migrations
78
+
79
+ - None.
80
+
7
81
  ## [0.4.5] - 2026-11-07
8
82
 
9
83
  Sign in with GitHub as the owner, from your other devices; approve a
package/README.md CHANGED
@@ -10,10 +10,24 @@
10
10
  </p>
11
11
 
12
12
  <picture>
13
- <source media="(prefers-color-scheme: dark)" srcset="docs/images/0.4.5/hero-dark.png">
14
- <img src="docs/images/0.4.5/hero-light.png" width="100%" alt="Den's Graph page: the demo workspace's services grouped by application, all healthy, with the status chip and the left rail">
13
+ <source media="(prefers-color-scheme: dark)" srcset="docs/images/0.4.6/hero-dark.png">
14
+ <img src="docs/images/0.4.6/hero-light.png" width="100%" alt="Den's Graph page: the demo workspace's services grouped by application, all healthy, with the status chip and the left rail">
15
15
  </picture>
16
16
 
17
+ ## Set up by your agent
18
+
19
+ Paste this line into your coding agent (Claude Code, Codex, Gemini, Grok, …):
20
+
21
+ ```text
22
+ Set up Den for me by following https://raw.githubusercontent.com/Monster-Orange/den/main/skills/install-den/SKILL.md
23
+ ```
24
+
25
+ The agent follows the [install-den skill](skills/install-den/SKILL.md). It asks you at most three questions,
26
+ creates your Den, adds one of your repos without changing it, starts it, checks it, and connects itself to Den's
27
+ MCP server. Then it tells you what's running and the URL to open. It never asks for a password in chat, never
28
+ edits your repos and sends nothing off your machine. Every step it runs answers in JSON and is safe to re-run:
29
+ see [Set up by your agent](docs/distributions.md#set-up-by-your-agent).
30
+
17
31
  ## What Den is
18
32
 
19
33
  Den is an opinionated local development framework for solo developers and small teams. You point it at the
@@ -24,7 +38,7 @@ It doesn't replace your tools. Your repos, Docker, your test runners, your secre
24
38
  yours. Den connects them and turns on good practice from the start.
25
39
 
26
40
  <p align="center">
27
- <img src="docs/images/0.4.5/up.gif" width="100%" alt="make up in the Graph page: every stack down, then demo-api, demo-web and demo-notes come up in dependency order until the status chip reads All 3 healthy">
41
+ <img src="docs/images/0.4.6/up.gif" width="100%" alt="make up in the Graph page: every stack down, then demo-api, demo-web and demo-notes come up in dependency order until the status chip reads All 3 healthy">
28
42
  </p>
29
43
 
30
44
  ## How it helps
@@ -54,10 +68,10 @@ yours. Den connects them and turns on good practice from the start.
54
68
  lives in your own repo.
55
69
 
56
70
  <p align="center">
57
- <img src="docs/images/0.4.5/agents.png" width="49%" alt="The Agents tab: the demo workspace's sessions on the left, and one open in a terminal pane on the right, running demo-api's test and then the service">
58
- <img src="docs/images/0.4.5/workspaces.png" width="49%" alt="The Workspaces page: the demo workspace's repos with their health, mode and Start, Stop, Restart and Logs actions">
59
- <img src="docs/images/0.4.5/status-drawer.png" width="49%" alt="The status drawer: nothing needs attention, and every service in the demo workspace, by application, with its latency">
60
- <img src="docs/images/0.4.5/edge.png" width="49%" alt="The Edge page: the edge container up, its ports and TLS, and every route it serves, Den's own and each stack's, with its upstream">
71
+ <img src="docs/images/0.4.6/agents.png" width="49%" alt="The Agents tab: the demo workspace's sessions on the left, and one open in a terminal pane on the right, running demo-api's test and then the service">
72
+ <img src="docs/images/0.4.6/workspaces.png" width="49%" alt="The Workspaces page: the demo workspace's repos with their health, mode and Start, Stop, Restart and Logs actions">
73
+ <img src="docs/images/0.4.6/status-drawer.png" width="49%" alt="The status drawer: nothing needs attention, and every service in the demo workspace, by application, with its latency">
74
+ <img src="docs/images/0.4.6/edge.png" width="49%" alt="The Edge page: the edge container up, its ports and TLS, and every route it serves, Den's own and each stack's, with its upstream">
61
75
  </p>
62
76
 
63
77
  ## Get started
@@ -32,8 +32,22 @@ From a Den checkout, `./bin/den create acme ../acme` does the same.
32
32
  | `--title <title>` | `<Name>` Title Case | Brand name shown to people |
33
33
  | `--cli <cli>` | `<name>` | The command people type |
34
34
  | `--env-prefix <PREFIX>` | `<NAME>` | Env-var prefix (`ACME` → `ACME_HOME`) |
35
+ | `--yes`, `-y` | | Never prompt (create never does) |
36
+ | `--json` | | One JSON object on stdout |
37
+ | `--port <n>` | `7777`, else free | The web UI's port |
35
38
 
36
39
  `create` refuses a directory that is not empty and never overwrites a file.
40
+ A directory that already holds this distribution (its `den.yml` names the
41
+ same product and command) is not an error: `create` says so and writes
42
+ nothing, so it is safe to re-run.
43
+
44
+ The web UI's port is 7777 unless that port is in use on this machine (another
45
+ Den already running, say). Then `create` picks the next free port above it,
46
+ never a port Den itself reserves (7778, 7791, 7799), writes it to the new
47
+ distribution's `config/local/user.yml` as `web: { port: N }`, and says so.
48
+ `--port <n>` sets it outright. The check only connects and binds; it sends
49
+ nothing to whatever holds the port. `DEN_WEB_PORT_START` moves the starting
50
+ port (tests use it so none touches 7777).
37
51
  [`examples/distribution/`](../examples/distribution) is exactly the output of
38
52
  `den create acme`; a unit test regenerates it, so the two never drift
39
53
  (`npm run example:regen` after changing the scaffolder).
@@ -48,6 +62,85 @@ npm install
48
62
  make help
49
63
  ```
50
64
 
65
+ ## Set up by your agent
66
+
67
+ A developer pastes one line into their agent:
68
+
69
+ ```text
70
+ Set up Den for me by following https://raw.githubusercontent.com/Monster-Orange/den/main/skills/install-den/SKILL.md
71
+ ```
72
+
73
+ That file is the install-den skill, also in the package at
74
+ `skills/install-den/SKILL.md`: plain Markdown any agent can follow, with
75
+ Claude skill front matter. `den --help` prints the line. The skill runs the
76
+ commands below. Each one takes `--json`, never prompts, and is safe to
77
+ re-run. With `--json`, stdout carries exactly one JSON object and the
78
+ command's log goes to stderr. When `ok` is false the object says why
79
+ (`error`) and carries either a command the agent can run (`fix`) or a
80
+ question for the person (`ask`, sometimes with `choices`).
81
+
82
+ | Step | Answers |
83
+ | ---------------------------------------------------------- | ---------------------------------------------------------------------------- |
84
+ | `npx @monster-orange/den create <name> [dir] --yes --json` | `{ ok, dir, cli, existed, files, port, portPicked?, next }` |
85
+ | `<cli> repo add <path\|git-url> --json` | `{ ok, existed, repo, workspace, mode, path, manifest, services, next }` |
86
+ | `<cli> up <repo> --json` | `{ ok, workspace, services: [{ address, result, health, detail? }], next }` |
87
+ | `<cli> web up --no-open --json` | `{ ok, url, port, version? }` |
88
+ | `<cli> doctor --json` | `{ ok, checks: [{ id, status, detail, url?, port?, fix?, ask? }] }` |
89
+ | `<cli> mcp connect --runtime <runtime> --json` | `{ ok, server, clients: [{ id, ok, said, file?, changed? }], restart, web }` |
90
+
91
+ ### repo add
92
+
93
+ `<cli> repo add <path|git-url> [--sidecar] [-w <workspace>] [--name <repo>]
94
+ [--run "<cmd>"] [--down "<cmd>"] [--health <url>] [--branch <branch>]
95
+ [--json]` adds a repo to a workspace without writing into the repo.
96
+
97
+ - A path is the person's own checkout, added dev-managed at that path. A git
98
+ URL is cloned where Den keeps its managed clones
99
+ (`.<name>-services/<repo>/<branch>`, the remote's default branch unless
100
+ `--branch` says otherwise) and added managed. A clone that is already
101
+ there is used as it is.
102
+ - A repo with its own manifest is used as it is. One without, or with
103
+ `--sidecar`, gets a side-car: a held manifest at
104
+ `manifests/<repo>.<name>.yml` ([below](#overriding-a-manifest)), keyed by
105
+ the repo's `origin` name or folder name. It is checked by the manifest
106
+ validator before it is written, and an existing one is never overwritten.
107
+ `--sidecar` on a repo with its own manifest is an error: Den always uses
108
+ the repo's own file (change it with an override instead).
109
+ - The side-car's commands are detected from the repo's files, in this order:
110
+ a Makefile with an `up` target (`make up`, plus `down`, `setup` and
111
+ `restart` when present); otherwise a compose file
112
+ (`docker compose -f <file> up -d` and `down`) or `package.json`'s `dev`,
113
+ else `start`, script. One candidate decides. Two, or none, is a question:
114
+ the answer has `ask` and `choices`, nothing is written, and a re-run names
115
+ the command with `--run "<cmd>"`. A Makefile `test` target or a
116
+ `package.json` `test` script becomes the repo's `tests.unit`.
117
+ `--health <url>` adds an HTTP health check.
118
+ - The workspace is `-w <workspace>` (made and activated if it doesn't exist),
119
+ else the focused one, else a new `default` workspace.
120
+ - The same repo again answers `ok` with `existed: true`. A different repo
121
+ under the same name is an error that offers `relocate` or `--name`.
122
+
123
+ ### up, doctor and mcp connect
124
+
125
+ - `up --json` runs `up` as usual, then reports each service it touched:
126
+ `result` is `started`, `already-running`, `failed` or `skipped`, and
127
+ `health` is the service's health after it started (a service with a
128
+ health check is waited on, up to 30 seconds). A healthy service is
129
+ `already-running` on a re-run.
130
+ - `doctor --json` lists the onboarding checks first: `node` (22.13 or
131
+ later), `git`, `docker` (only when a registered repo has a compose file or
132
+ block, or a docker command), `web` (the web server answers at `url`) and
133
+ `mcp` (`<cli> mcp --call workspaces` gets an answer through it). Then one
134
+ `strategy:<kind>` check per row of the [human doctor](#doctor). `ok` is
135
+ false, and the exit code 1, when any check is `fail`.
136
+ - `mcp connect --runtime <runtime>` names the agent by its runtime: `claude`
137
+ (the `claude-code` client), `codex`, `gemini` or `grok`. Grok CLI keeps its
138
+ MCP servers in `~/.grok/config.toml` (or `.grok/config.toml` with
139
+ `--project`), one `[mcp_servers.<name>]` table each
140
+ ([xAI's docs](https://docs.x.ai/build/features/mcp-servers)); Den writes
141
+ only its own table. A config file that already has the entry is left as
142
+ it is (`changed: false`).
143
+
51
144
  ## Layout
52
145
 
53
146
  | Path | What |
@@ -587,6 +680,10 @@ Acme doctor: strategies
587
680
  `session-backend` is never a doctor failure: nothing needs a session until
588
681
  one is started (see Sessions).
589
682
 
683
+ `<cli> doctor --json` adds the onboarding checks (Node, git, Docker, the web
684
+ server, Den's MCP) and answers with one JSON object: see
685
+ [Set up by your agent](#up-doctor-and-mcp-connect).
686
+
590
687
  ## den.yml: the product profile
591
688
 
592
689
  Every runtime name derives from `product.name`. Set only what you want to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monster-orange/den",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "description": "Den: a local multi-repo dev orchestrator, HTTPS edge and E2E harness",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,7 +25,8 @@
25
25
  "README.md",
26
26
  "CHANGELOG.md",
27
27
  "docs/",
28
- "!docs/images/"
28
+ "!docs/images/",
29
+ "skills/"
29
30
  ],
30
31
  "scripts": {
31
32
  "cli": "tsx src/ui/cli/index.ts menu",
@@ -44,7 +45,7 @@
44
45
  "gate:org-names": "node scripts/org-name-gate.mjs",
45
46
  "docs:shots": "node scripts/doc-shots.mjs",
46
47
  "docs:links": "node scripts/docs-links.js",
47
- "example:regen": "rm -rf examples/distribution && ./bin/den create acme examples/distribution",
48
+ "example:regen": "rm -rf examples/distribution && ./bin/den create acme examples/distribution --port 7777",
48
49
  "dist-smoke": "bash scripts/dist-smoke.sh"
49
50
  },
50
51
  "keywords": [
@@ -0,0 +1,114 @@
1
+ ---
2
+ name: install-den
3
+ description: Install and set up Den, Monster Orange's local dev orchestrator, for a developer in five minutes. Use when the person says "set up Den", "install Den", "get me started with Den", or pastes a link to this file.
4
+ ---
5
+
6
+ # Install Den
7
+
8
+ You are setting up Den on this person's machine. Den runs their repos locally, shows them in a browser, and gives you
9
+ (the agent) an MCP server to work with. Aim to be done in five minutes. Keep your messages short and plain.
10
+
11
+ ## Hard rules
12
+
13
+ - **Never ask for a token, password or secret in chat.** If a step needs one, tell the person which command to run
14
+ themselves.
15
+ - **Never edit the person's repos.** Den's files go in the Den folder only. Read the repo; don't write to it.
16
+ - **Send nothing off this machine.** No uploads, no telemetry, no pasting their code anywhere. The only downloads are
17
+ Den from npm and a repo the person gave you as a git URL.
18
+ - **Sudo is the person's job.** Never run `sudo` yourself. When a Den step needs it, Den asks for the password in its
19
+ own prompt (in the terminal or the browser), and the person types it there.
20
+ - Every step below is safe to re-run. If you are unsure whether a step ran, run it again.
21
+
22
+ ## 1. Check the prerequisites
23
+
24
+ Run these and read the results. Don't install anything without saying so first.
25
+
26
+ 1. `node --version`: needs **22.13 or later**. If it's older or missing, say so and suggest their usual installer
27
+ (for example `brew install node`, or `nvm install 22`).
28
+ 2. `npm --version`: needs 9 or later. It comes with Node.
29
+ 3. `git --version`: needs to be there.
30
+ 4. `docker info`: only matters if the repo runs in containers (it has a `docker-compose.yml` or `compose.yaml`).
31
+ If Docker is needed and not running, ask them to start it.
32
+
33
+ If anything is missing, stop here, tell them what and how to fix it, and wait.
34
+
35
+ ## 2. Ask at most three questions
36
+
37
+ Ask them together, each with a default, so the person can just say "yes":
38
+
39
+ 1. **Where should your Den live?** Default: `~/den`. It must be a new or empty folder.
40
+ 2. **Which repo should we start with?** Default: the folder you're in now. A local path or a git URL.
41
+ 3. **What should we call your Den?** Default: `den`. Lowercase letters, numbers and dashes. This also becomes the
42
+ command name, so `acme` gives you `./bin/acme`.
43
+
44
+ Below, `<dir>` is the Den folder, `<repo>` the repo path, and `<name>` the name.
45
+
46
+ ## 3. Create the Den
47
+
48
+ Every Den command below takes `--json`. It then prints one JSON object on stdout and its log on stderr. Read `ok`
49
+ first. When `ok` is false, the object says why (`error`) and either a command to run (`fix`) or a question for the
50
+ person (`ask`, sometimes with `choices`).
51
+
52
+ 1. `npx @monster-orange/den create <name> <dir> --yes --json`. It answers `{ok, dir, cli, existed, port, next}`. If
53
+ the folder already holds this Den, it answers `ok` with `existed: true` and changes nothing. `port` is the browser
54
+ UI's port. When another Den already uses the usual one on this machine, Den picks the next free port and says so
55
+ (`portPicked`). Remember the port for the hand-back.
56
+ 2. `cd <dir> && npm install`
57
+ 3. From here on, the Den command is `./bin/<name>`, run from `<dir>`.
58
+
59
+ ## 4. Add the repo without changing it
60
+
61
+ 1. `./bin/<name> repo add <repo> --json`. `<repo>` can be a path or a git URL. Den clones a URL into its own folder.
62
+ - If the repo has its own `.<name>.yml`, Den uses it.
63
+ - If it doesn't, Den works out how the repo starts and writes a side-car manifest into `<dir>/manifests/`. It never
64
+ writes into the repo. It looks for a Makefile `up` target, then a compose file, then the `dev` or `start` script
65
+ in `package.json`. `--sidecar` asks for this explicitly, and fails if the repo has its own manifest.
66
+ - If you can see which port the app listens on, add `--health http://localhost:<port>/`. Den then checks that it
67
+ answers.
68
+ 2. If the answer has `ask` and `choices`, Den couldn't tell how the repo starts, and it wrote nothing. Ask the person
69
+ **one** question: the `ask` text, with the choices numbered. Then re-run with their answer:
70
+ `./bin/<name> repo add <repo> --run "<their command>" --json`.
71
+ 3. The answer names the repo (`repo`), the workspace it joined (`workspace`) and where its manifest is (`manifest`).
72
+ A workspace is Den's name for a set of repos that run together. The first one is called `default`.
73
+
74
+ ## 5. Start it and check it
75
+
76
+ 1. `./bin/<name> up <repo> --json`. `<repo>` is the `repo` value from step 4. Each service comes back with a
77
+ `result` (`started` or `already-running`) and a `health` (`ok`, or `unknown` when it has no health check).
78
+ 2. `./bin/<name> web up --no-open --json`. This starts the browser UI, and answers `{ok, url, port}`. The MCP server
79
+ reads through it.
80
+ 3. `./bin/<name> doctor --json`. Each check comes back as `ok`, `warn` or `fail`. The checks are Node, git, Docker
81
+ (only if a repo needs it), the web server (with its `url` and `port`) and Den's MCP. Fix every `fail` before you hand back.
82
+
83
+ ## 6. Connect yourself to Den's MCP
84
+
85
+ 1. `./bin/<name> mcp connect --runtime <you> --json`. `<you>` is `claude`, `codex`, `gemini` or `grok`. For any other
86
+ agent, `./bin/<name> mcp config` prints the JSON to add by hand.
87
+ 2. `./bin/<name> mcp clients` shows whether you are connected.
88
+ 3. If the answer has `restart: true`, you load the new MCP server only when you start again. Tell the person to
89
+ restart you once.
90
+
91
+ ## 7. Hand back
92
+
93
+ Tell the person, in a few short sentences. Explain every name you use the first time (for example, "a workspace is a
94
+ set of repos that run together").
95
+
96
+ - **Open:** the `url` from `web up` or from the doctor's `web` check, for example `http://localhost:<port>`. Never
97
+ assume a port: use the one Den gave you.
98
+ - **What's running:** each service from `up`, and whether it's healthy.
99
+ - **Your Den:** where it lives (`<dir>`) and its command (`./bin/<name>`).
100
+ - **Decisions, if any:** as numbered yes/no questions, for example "1. Restart me now so I can use Den's tools?"
101
+ - **Three next things we can do together:**
102
+ 1. Add another repo.
103
+ 2. Look at a service's logs, or run its tests.
104
+ 3. Start an agent session on this repo from the browser.
105
+
106
+ ## When something fails
107
+
108
+ 1. Read the JSON. If it has `fix`, run that command, then re-run the step that failed.
109
+ 2. If it has `ask`, the person has to decide or act (sudo, a password, starting Docker, signing in). Ask them the
110
+ question, show any `choices` numbered, and wait.
111
+ 3. If there is neither, ask the person **one** clear question, with your best guess as the default.
112
+ 4. Never work around a failure by editing their repo, turning off a check, or skipping a step without saying so.
113
+ 5. If you're stuck after two tries, stop. Tell them what you ran, what it said, and where Den writes its logs
114
+ (`~/.<name>/`), so they can get help.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The one line a developer pastes into their agent to have it set Den up
3
+ * (ON-5): the agent follows the install-den skill published at this URL
4
+ * (skills/install-den/SKILL.md in the package).
5
+ */
6
+ export const AGENT_SETUP_URL =
7
+ "https://raw.githubusercontent.com/Monster-Orange/den/main/skills/install-den/SKILL.md";
8
+
9
+ export const AGENT_SETUP_LINE = `Set up Den for me by following ${AGENT_SETUP_URL}`;
@@ -112,3 +112,10 @@ export const SKIP_TEST_DIRS = [
112
112
 
113
113
  /** Default den web server port. */
114
114
  export const DEFAULT_PORT = 7777;
115
+
116
+ /**
117
+ * Ports a new distribution's web server never takes when `create` picks
118
+ * one because DEFAULT_PORT is in use: the edge's and other well-known Den
119
+ * instances' ports.
120
+ */
121
+ export const RESERVED_DEN_PORTS: readonly number[] = [7778, 7791, 7799];
@@ -31,7 +31,7 @@
31
31
 
32
32
  import * as fs from "fs";
33
33
  import * as path from "path";
34
- import { execFileSync } from "child_process";
34
+ import { execFileSync, spawn } from "child_process";
35
35
  import { createHash, randomUUID } from "crypto";
36
36
  import * as yaml from "yaml";
37
37
  import { getDistRoot } from "../paths.js";
@@ -241,6 +241,32 @@ export function buildEnvelope(
241
241
  return `{}\n${itemHeader}\n${payload}\n`;
242
242
  }
243
243
 
244
+ /**
245
+ * Send from a short-lived detached process instead of this one. The CLI
246
+ * records its run in the `exit` handler, where an async fetch never runs;
247
+ * a detached sender outlives the CLI and gives up after SHIP_TIMEOUT_MS.
248
+ * The long-lived server keeps the in-process fetch.
249
+ */
250
+ let shipDetached = false;
251
+ export function shipFromDetachedSender(): void {
252
+ shipDetached = true;
253
+ }
254
+
255
+ const SENDER = `fetch(process.env.DM_URL,{method:"POST",headers:{"Content-Type":"application/x-sentry-envelope","X-Sentry-Auth":process.env.DM_AUTH},body:process.env.DM_BODY,signal:AbortSignal.timeout(${SHIP_TIMEOUT_MS})}).catch(()=>{}).finally(()=>process.exit(0))`;
256
+
257
+ function sendDetached(url: string, auth: string, body: string): void {
258
+ try {
259
+ spawn(process.execPath, ["-e", SENDER], {
260
+ detached: true,
261
+ stdio: "ignore",
262
+ // Only what the sender needs: none of the caller's environment.
263
+ env: { DM_URL: url, DM_AUTH: auth, DM_BODY: body },
264
+ }).unref();
265
+ } catch {
266
+ /* fail-open: the JSONL line is already buffered */
267
+ }
268
+ }
269
+
244
270
  function ship(
245
271
  record: Record<string, string>,
246
272
  epoch: number,
@@ -266,6 +292,7 @@ function ship(
266
292
 
267
293
  const url = `${parsed.scheme}://${parsed.host}/api/${parsed.projectId}/envelope/`;
268
294
  const auth = `Sentry sentry_key=${parsed.key}, sentry_version=7, sentry_client=dev-metrics-${product().cli}/1.0`;
295
+ if (shipDetached) return sendDetached(url, auth, envelope);
269
296
  // Fire-and-forget: never awaited, so the caller never blocks. Bounded by a 2s
270
297
  // abort so a hung ingest can't leak a pending request in the long-lived server.
271
298
  void fetch(url, {
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Picking a free localhost port for a new distribution's web server
3
+ * (`create`, ON-2), so a second Den on one machine doesn't collide with the
4
+ * first on DEFAULT_PORT.
5
+ *
6
+ * "In use" is checked without talking to whoever holds the port: a TCP
7
+ * connect that is closed as soon as it opens (no byte is sent), then a
8
+ * bind. Either one finding something there means in use.
9
+ */
10
+ import * as net from "net";
11
+ import { RESERVED_DEN_PORTS } from "./constants.js";
12
+
13
+ /** Whether something holds `port` on localhost. Never sends a byte. */
14
+ export function portInUse(port: number, host = "localhost"): Promise<boolean> {
15
+ return new Promise((resolve) => {
16
+ const sock = net.connect({ host, port });
17
+ const done = (inUse: boolean | null) => {
18
+ sock.destroy();
19
+ if (inUse !== null) return resolve(inUse);
20
+ // Nothing accepted a connection: can we bind it ourselves?
21
+ const srv = net.createServer();
22
+ srv.once("error", () => resolve(true));
23
+ srv.listen({ port, host, exclusive: true }, () =>
24
+ srv.close(() => resolve(false)),
25
+ );
26
+ };
27
+ sock.setTimeout(1_000);
28
+ sock.once("connect", () => done(true));
29
+ // Something there that doesn't complete a handshake: in use.
30
+ sock.once("timeout", () => done(true));
31
+ sock.once("error", (e: NodeJS.ErrnoException) =>
32
+ done(e.code === "ECONNREFUSED" ? null : true),
33
+ );
34
+ });
35
+ }
36
+
37
+ export interface PickPortOptions {
38
+ /** The port wanted (the default); the search goes up from here. */
39
+ start: number;
40
+ /** Never chosen. */
41
+ reserved?: readonly number[];
42
+ /** How many ports above `start` to try. */
43
+ span?: number;
44
+ inUse?: (port: number) => Promise<boolean>;
45
+ }
46
+
47
+ /**
48
+ * `start` when it is free, else the first free port above it that is not
49
+ * reserved; null when none in the span is free.
50
+ */
51
+ export async function pickFreePort(o: PickPortOptions): Promise<number | null> {
52
+ const reserved = o.reserved ?? RESERVED_DEN_PORTS;
53
+ const inUse = o.inUse ?? ((p: number) => portInUse(p));
54
+ const span = o.span ?? 100;
55
+ for (let p = o.start; p <= Math.min(o.start + span, 65535); p++) {
56
+ if (reserved.includes(p)) continue;
57
+ if (!(await inUse(p))) return p;
58
+ }
59
+ return null;
60
+ }
@@ -6,4 +6,4 @@
6
6
  import { runCreate } from "./create.js";
7
7
 
8
8
  const cwd = process.env.DEN_INVOKE_CWD?.trim() || process.cwd();
9
- process.exit(runCreate(process.argv.slice(2), cwd));
9
+ process.exit(await runCreate(process.argv.slice(2), cwd));