@deepseek-insight/dsi 0.6.14 → 0.6.16
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/.agents/skills/dsi-ports/SKILL.md +61 -0
- package/.agents/skills/dsi-restart/SKILL.md +85 -0
- package/bin/dsi.mjs +148 -39
- package/bin/lib/dev-mode.mjs +138 -0
- package/bin/lib/plugin-chain.mjs +47 -0
- package/build/client/_app/immutable/assets/0.KhAZv9mf.css +2 -0
- package/build/client/_app/immutable/assets/2.yPxdQ-n0.css +1 -0
- package/build/client/_app/immutable/chunks/{CiLhSVIj.js → -izoh3h0.js} +1 -1
- package/build/client/_app/immutable/chunks/{Dj4_qcIH.js → B-V7Vlsz.js} +4 -4
- package/build/client/_app/immutable/chunks/{CP4wchdA.js → BXGojodM.js} +2 -2
- package/build/client/_app/immutable/chunks/C0ZiNuOj.js +1 -0
- package/build/client/_app/immutable/chunks/{C_Gd0Wor.js → CbtU_GW3.js} +1 -1
- package/build/client/_app/immutable/chunks/{GdM4UPkS.js → Ck2086Sr.js} +1 -1
- package/build/client/_app/immutable/chunks/{C8seIiU_.js → D3rUyYDm.js} +1 -1
- package/build/client/_app/immutable/chunks/{uEyn1Z7p.js → DOkUpqkd.js} +1 -1
- package/build/client/_app/immutable/chunks/b58grRSF.js +1 -0
- package/build/client/_app/immutable/chunks/{2KTqoQH-.js → q1Yj7q99.js} +1 -1
- package/build/client/_app/immutable/entry/{app.CCuVGpQc.js → app.CfbFUHxw.js} +2 -2
- package/build/client/_app/immutable/entry/start.4M41VX7w.js +1 -0
- package/build/client/_app/immutable/nodes/{1.CaQ_jAq8.js → 1.C2BfYC2f.js} +1 -1
- package/build/client/_app/immutable/nodes/{2.DxOLlTKc.js → 2.UdTbIvwR.js} +220 -220
- package/build/client/_app/version.json +1 -1
- package/build/handler.js +4 -4
- package/build/index.js +4 -4
- package/build/server/chunks/chunks/{dsh-connection.js-DRtqPR_j.js → dsh-connection.js-DvncrWM4.js} +5 -2
- package/build/server/chunks/chunks/{git-probe.js-Daf6r7Z9.js → git-probe.js-S9w9gGXE.js} +2 -2
- package/build/server/chunks/chunks/{internal2.js-DAPn3Vk8.js → internal2.js-DMu68nYR.js} +2 -2
- package/build/server/chunks/chunks/restart-chain.js-D-VwGWE2.js +112 -0
- package/build/server/chunks/chunks/{state.js-CsIZcr7q.js → state.js-CBD6RE0B.js} +2 -2
- package/build/server/chunks/chunks/{watcher.js-CoR53lwR.js → watcher.js-BfZOp5td.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/a2a/{_server.ts.js-BKlF4XGH.js → _server.ts.js-D9vpMh8K.js} +3 -3
- package/build/server/chunks/entries/endpoints/api/a2a/register/{_server.ts.js-BpEhlidV.js → _server.ts.js-HPJEmfbv.js} +3 -3
- package/build/server/chunks/entries/endpoints/api/dsh/directory/{_server.ts.js-Dmh7f-hU.js → _server.ts.js-BaAmRXrU.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/dsh/pick-directory/{_server.ts.js-BEci0ZAx.js → _server.ts.js-D-Ftq5ei.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/dsh/presets/{_server.ts.js-OXFzXBSp.js → _server.ts.js-C0_j53OW.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/attachment/{_server.ts.js-x44GKy4R.js → _server.ts.js-CxMvUVdS.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/cancel/{_server.ts.js-Da5ZqeJ7.js → _server.ts.js-CfjRtKMp.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/catalog/{_server.ts.js-BACv9hW-.js → _server.ts.js-D7uC5b_j.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/changes/{_server.ts.js-BtiUS3iD.js → _server.ts.js-TeoUfrIl.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/command/{_server.ts.js-BOOyd1LI.js → _server.ts.js-uqUb-EdM.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/events/{_server.ts.js-D0GwQbdF.js → _server.ts.js-C_DF0fAc.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/fork/{_server.ts.js-Bd0JYMlf.js → _server.ts.js-BfkqDQXU.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/goal/{_server.ts.js-DqAV7OOv.js → _server.ts.js-vCfxCACS.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/history/{_server.ts.js-Cz4P-68E.js → _server.ts.js-DJlpW4hl.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/models/{_server.ts.js-C4fXXBIM.js → _server.ts.js-Cqn5ZMGh.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/permission/{_server.ts.js-BiqNZiiN.js → _server.ts.js-ChB_0ZdM.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/prompt/{_server.ts.js-Bdx9V4oY.js → _server.ts.js-BVHypcWF.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/rename/{_server.ts.js-CuBqoV3c.js → _server.ts.js-BwDVfVrR.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/respond/{_server.ts.js-CXPFI_Pn.js → _server.ts.js-DRmd83Ea.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/select-model/{_server.ts.js-CRcaw4Yo.js → _server.ts.js-hc61Nb64.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/dsh/sessions/{_server.ts.js-dYrtVHDP.js → _server.ts.js-xkp4IT7y.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/dsh/workspace-file/{_server.ts.js-BtlfuMjf.js → _server.ts.js-BvPKBiTg.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/dsh/workspace-tree/{_server.ts.js-D12kAP9B.js → _server.ts.js-BSsF01K6.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/dsh/workspaces/{_server.ts.js-C9JHp1WU.js → _server.ts.js-DNLxFs1R.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/dsh/workspaces/_workspaceId_/delete/{_server.ts.js-bLF4Fbgn.js → _server.ts.js-5jaRPaKn.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/dsh/workspaces/_workspaceId_/rename/{_server.ts.js-BSw_zD-0.js → _server.ts.js-A-hvBqyy.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/floor/mode/_server.ts.js-CzBvebv2.js +25 -0
- package/build/server/chunks/entries/endpoints/api/floor/restart/_server.ts.js-D5wnUph-.js +30 -0
- package/build/server/chunks/entries/endpoints/api/plugins/apply/{_server.ts.js-BNEUWbaf.js → _server.ts.js-CCs56XtH.js} +5 -56
- package/build/server/chunks/entries/endpoints/api/workspace/file-write/{_server.ts.js-CqyKA9g2.js → _server.ts.js-C0x68iMD.js} +3 -3
- package/build/server/chunks/entries/endpoints/api/workspace/git-events/{_server.ts.js-yu695YoE.js → _server.ts.js-DzdGKZdV.js} +3 -3
- package/build/server/chunks/entries/endpoints/api/workspace/git-file-head/{_server.ts.js-BhDAvM6y.js → _server.ts.js-BC1jb3zy.js} +3 -3
- package/build/server/chunks/entries/endpoints/api/workspace/git-map/{_server.ts.js-BRUgPrJ2.js → _server.ts.js-BE9XJyhN.js} +3 -3
- package/build/server/chunks/entries/endpoints/api/workspace/git-status/{_server.ts.js-C7VJ2G4_.js → _server.ts.js-CWlGtOu8.js} +3 -3
- package/build/server/chunks/entries/endpoints/api/workspace/write-gate/{_server.ts.js-BH5BRA_d.js → _server.ts.js-CgcggNrH.js} +3 -3
- package/build/server/chunks/entries/pages/{_error.svelte.js-B5TEtccE.js → _error.svelte.js-CKrYUsNh.js} +3 -3
- package/build/server/chunks/entries/pages/{_page.server.ts.js-CityLNBO.js → _page.server.ts.js-CbzURHkB.js} +2 -2
- package/build/server/chunks/entries/pages/{_page.svelte.js-i6KsWK9O.js → _page.svelte.js-Bub68nnw.js} +218 -104
- package/build/server/chunks/{handler-XuK3Ihhf.js → handler-29vKRsIm.js} +3 -3
- package/build/server/chunks/{index.js-D2FEiD0I.js → index.js-BpxR9RqE.js} +2 -2
- package/build/server/chunks/{manifest.js-B_kahcFI.js → manifest.js-BUA2pFJz.js} +52 -38
- package/build/server/chunks/nodes/{0.js-C0B2o0le.js → 0.js-zSkx80Fq.js} +3 -3
- package/build/server/chunks/nodes/1.js-BFDsDe_G.js +9 -0
- package/build/server/chunks/nodes/{2.js-Bh3kLeYq.js → 2.js-B3UD9xCC.js} +6 -6
- package/package.json +1 -1
- package/template/.dsi/prompts.sqlite +0 -0
- package/template/.dsi/prompts.sqlite-shm +0 -0
- package/template/.dsi/prompts.sqlite-wal +0 -0
- package/.agents/skills/dsi-thin/SKILL.md +0 -140
- package/.agents/skills/dsi-thin/references/ADR-0019-snapshot.md +0 -363
- package/.agents/skills/dsi-thin/references/authority-adr.md +0 -28
- package/.agents/skills/dsi-thin/references/input-contract.md +0 -42
- package/.agents/skills/dsi-thin/references/proven-gates.md +0 -61
- package/.agents/skills/dsi-thin/references/two-lane-contract.md +0 -53
- package/build/client/_app/immutable/assets/0.B-dIufDc.css +0 -2
- package/build/client/_app/immutable/assets/2.B70dfiy3.css +0 -1
- package/build/client/_app/immutable/chunks/38H6DLwz.js +0 -1
- package/build/client/_app/immutable/chunks/DJWLLhNJ.js +0 -1
- package/build/client/_app/immutable/entry/start.BtgtnpyB.js +0 -1
- package/build/server/chunks/nodes/1.js-C2e7NFqd.js +0 -9
- /package/build/client/_app/immutable/nodes/{0.GHtnahSb.js → 0.Jl8y_yU6.js} +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dsi-ports
|
|
3
|
+
description: Fast feedback-loop port inventory — run /dsi-ports (or ask "what's running / which ports are open / what's eating memory") to list every TCP listener with pid, process, memory usage, and its purpose (DSH host, DSI UI, Playwright, inspector, test floors). Backed by the Port Ledger KB.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# dsi-ports — the Port Ledger, live
|
|
7
|
+
|
|
8
|
+
Companion KB: `dev/kb/open-ports/2026-10-12 - The Port Ledger — Who Opens What, Why, and How Test Floors Stay Isolated.md` — read it for WHY each port exists. This skill is the LOOK-path: one command, one table, no judgment.
|
|
9
|
+
|
|
10
|
+
## Run
|
|
11
|
+
|
|
12
|
+
Execute this single pipeline (macOS; safe, read-only). AGENT HYGIENE: write
|
|
13
|
+
the block to a temp file (`/tmp/dsi-ports.sh`) and run `sh` on it — inlining
|
|
14
|
+
shell `$-expansions` inside a JS template string breaks the tool call
|
|
15
|
+
(observed 2026-10-12); the failure is the caller's, not the pipeline's.
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
for pid_port in $(lsof -nP -iTCP -sTCP:LISTEN | awk 'NR>1 {match($9, /[^:]*$/); print substr($9, RSTART) "/" $2}' | sort -u); do
|
|
19
|
+
port=${pid_port%%/*}; pid=${pid_port##*/}
|
|
20
|
+
rss=$(ps -o rss= -p "$pid" 2>/dev/null | awk '{printf "%.0f MB", $1/1024}')
|
|
21
|
+
cmd=$(ps -o command= -p "$pid" 2>/dev/null | cut -c1-70)
|
|
22
|
+
echo "$port | $pid | ${rss:-?} | $cmd"
|
|
23
|
+
done | sort -n
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Format the output
|
|
27
|
+
|
|
28
|
+
Present EXACTLY this table (port ascending), then the purpose line:
|
|
29
|
+
|
|
30
|
+
| Port | PID | Process | Memory (RSS) | Purpose |
|
|
31
|
+
|---|---|---|---|---|
|
|
32
|
+
| 3080 | 1234 | node …/vite.js dev | 412 MB | **DSH host** — browser RPC + token announce |
|
|
33
|
+
| 5175 | 1234 | node …/vite.js dev | 388 MB | **DSI dev UI** — the page the operator uses |
|
|
34
|
+
|
|
35
|
+
Purpose lookup (from the Port Ledger KB — first match wins):
|
|
36
|
+
|
|
37
|
+
| Port | Purpose |
|
|
38
|
+
|---|---|
|
|
39
|
+
| 3080 | DSH host — browser RPC, token announce |
|
|
40
|
+
| 5174 | DSI built UI (production page) |
|
|
41
|
+
| 5175 | DSI dev UI (Vite, HMR) |
|
|
42
|
+
| 5176 | Playwright webServer (vite preview, e2e backend) |
|
|
43
|
+
| 9229 | Node inspector (D7 — one debugger for DSI + embedded host) |
|
|
44
|
+
| 1933 | OpenViking memory server |
|
|
45
|
+
| 34xx | test-floor DSH host (isolated) |
|
|
46
|
+
| 51xx (5180+) | test-floor UI (isolated) |
|
|
47
|
+
| anything else | **unknown — name the binary and ask** (never guess a purpose) |
|
|
48
|
+
|
|
49
|
+
## Reading the table (tell the operator, one line each)
|
|
50
|
+
|
|
51
|
+
- Same PID on TWO ports (e.g. 3080 + 5175) → the **in-process floor** (one process, D7).
|
|
52
|
+
- 5175 up but 3080 down → UI-only floor; the page will show "Failed to fetch".
|
|
53
|
+
- 3080 up but no 5175/5174 → bare host (tandem's other half missing, or runCli host only).
|
|
54
|
+
- Two PIDs on the same port → TWO listeners — list both rows, flag it.
|
|
55
|
+
- Biggest RSS → name it; vite dev floors routinely sit at 300–500 MB (normal).
|
|
56
|
+
|
|
57
|
+
## Boundaries
|
|
58
|
+
|
|
59
|
+
- Read-only: `lsof`, `ps`, `sort`. Never kill, never open, never edit — remediation is always a separate, confirmed step.
|
|
60
|
+
- IPv6 vs IPv4 duplicates: same port with different PIDs means TWO listeners — list both rows, flag it.
|
|
61
|
+
- The table is a snapshot; ports move between runs. Re-run, don't cache.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dsi-restart
|
|
3
|
+
description: Restart the DSI/DSH floor with a before/after report — /dsi-restart snapshots every listener's port+pid+RSS, restarts (or waits for the operator's restart), re-snapshots, and prints the delta table (which ports restarted, memory freed/claimed). Answers "floor feels heavy, restart and show me what changed".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# dsi-restart — restart with receipts
|
|
7
|
+
|
|
8
|
+
Companion: `dsi-ports` skill (the snapshot pipeline) and the Port Ledger KB (`dev/kb/open-ports/`). This skill adds the DELTA: before, restart, after, memory moved.
|
|
9
|
+
|
|
10
|
+
## 0. Identify how the floor is running (do not skip)
|
|
11
|
+
|
|
12
|
+
**Facts first (2026-10-12, hard rule):** classify launch shape ONLY from a full
|
|
13
|
+
ancestry walk — `ps -o pid,ppid,lstart,command` repeated up the ppid chain
|
|
14
|
+
until you reach pid 1 or an interactive `zsh`/`login`. Record each hop. An
|
|
15
|
+
immediate-parent glance is not detection: the same zsh can own both a dead
|
|
16
|
+
floor's corpse and the replacement the OPERATOR launched by hand (precedent
|
|
17
|
+
2026-10-12: the assistant claimed a "detached relaunch" that was actually the
|
|
18
|
+
operator re-running the command in their own terminal). **Never narrate a
|
|
19
|
+
relaunch you did not perform and cannot cite a pid + start time for.**
|
|
20
|
+
|
|
21
|
+
| Launch shape | Detection (full ancestry walk) | Who may restart it |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| Operator's foreground terminal | the `bin/dsi.mjs` chain's top parent is an interactive `zsh`/`login` (walk ppids; lstart proves which launch is which) | **the operator only** — or the Restart Verb with the operator's go (§2) |
|
|
24
|
+
| Detached chain (plugin-rack / verb bounce) | the chain's top parent is pid 1 (launchd) | the skill, via the Restart Verb or `--sync-kills` + relaunch |
|
|
25
|
+
|
|
26
|
+
If detection is ambiguous → **ask the operator** before killing anything (precedent: pid 37084, 2026-10-12 — the assistant killed the operator's live floor during cleanup).
|
|
27
|
+
|
|
28
|
+
**Exit-143 rule (2026-10-12, corrected):** when the floor dies — Ctrl-C, kill,
|
|
29
|
+
or the Restart Verb hitting a foreground-terminal floor — the terminal prints
|
|
30
|
+
`ELIFECYCLE Command failed with exit code 143` and the floor is **DOWN. It
|
|
31
|
+
does not come back by itself.** Only the detached restart chain (the verb's
|
|
32
|
+
spawned one-shot) or a human re-running the command brings it back. Never
|
|
33
|
+
write or say "the floor came back detached" without a pid + `lstart` you just
|
|
34
|
+
read from `ps` — an unverified rebirth story cost the operator a dead floor
|
|
35
|
+
and a wrong incident report (2026-10-12).
|
|
36
|
+
|
|
37
|
+
## 1. Snapshot BEFORE
|
|
38
|
+
|
|
39
|
+
Run the dsi-ports pipeline (see dsi-ports skill for the script) and save:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
sh /tmp/dsi-ports.sh > /tmp/floor-before.txt
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 2. Restart
|
|
46
|
+
|
|
47
|
+
- **PREFERRED, every shape: the Restart Verb (shipped — ADR "The Restart Verb,
|
|
48
|
+
One Button, One Route, Receipts", 2026-10).** With the operator's go, click
|
|
49
|
+
the power button in the sidebar header (or `POST /api/floor/restart`). It
|
|
50
|
+
spawns the same detached chain plugin-apply uses, so the floor re-serves in
|
|
51
|
+
the same mode (`--sync --dev in-process` fidelity) even when the previous
|
|
52
|
+
floor lived in the operator's terminal. Receipts still apply: BEFORE/AFTER
|
|
53
|
+
snapshots are mandatory.
|
|
54
|
+
- **Fallback, detached/agent-owned floor:** `dsi dsh --sync-kills`, then
|
|
55
|
+
relaunch the exact previous command (copy it from `ps` BEFORE killing).
|
|
56
|
+
- **Fallback, operator foreground floor (operator declines the verb):** print
|
|
57
|
+
the restart command and WAIT for the operator to run it (their Ctrl-C, their
|
|
58
|
+
terminal). Remember: once it dies, it stays dead until someone starts it.
|
|
59
|
+
|
|
60
|
+
## 3. Snapshot AFTER (wait for both listeners first)
|
|
61
|
+
|
|
62
|
+
Poll until the expected ports answer (host + UI), then:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
sh /tmp/dsi-ports.sh > /tmp/floor-after.txt
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 4. The delta report (this is the deliverable)
|
|
69
|
+
|
|
70
|
+
Present exactly this table — join BEFORE and AFTER on port purpose (not pid: pids always change):
|
|
71
|
+
|
|
72
|
+
| Port | Purpose | RSS before | RSS after | Delta | PID before → after |
|
|
73
|
+
|---|---|---|---|---|---|
|
|
74
|
+
| 3080 | DSH host | 3262 MB | 1489 MB | **−1773 MB** | 38237 → 41200 |
|
|
75
|
+
|
|
76
|
+
Then one summary line: total floor RSS before → after, and the verdict:
|
|
77
|
+
- large negative delta + all expected ports up → **restart healthy**
|
|
78
|
+
- a port missing after → **name it loud** (that half of the floor did not come back)
|
|
79
|
+
- RSS after ≥ before → say so honestly; restart did not reclaim (possible leak — offer dsi-rca)
|
|
80
|
+
|
|
81
|
+
## Boundaries
|
|
82
|
+
|
|
83
|
+
- Never kill a process the operator owns without their confirmed go (§0).
|
|
84
|
+
- Always restore the EXACT previous launch command/flags/env — a restart that changes the floor's shape is a config change, not a restart.
|
|
85
|
+
- The report is the point: a restart without before/after receipts is just downtime.
|
package/bin/dsi.mjs
CHANGED
|
@@ -28,7 +28,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
28
28
|
import { parse, stringify } from 'yaml';
|
|
29
29
|
|
|
30
30
|
import { mirrorSkills } from './lib/mirror-skills.mjs';
|
|
31
|
-
import { dshLauncherCmd, parsePluginArgs } from './lib/plugin-chain.mjs';
|
|
31
|
+
import { dshLauncherCmd, parsePluginArgs, takeoverFloorPorts } from './lib/plugin-chain.mjs';
|
|
32
|
+
import { parseDevArg, devModeEnv, modeFromBoolean, resolveBounceMode, extractInspectArgs, parseInspectArgsFromEnv } from './lib/dev-mode.mjs';
|
|
32
33
|
|
|
33
34
|
// Layman-facing help: wrapped near 76 columns, continuation lines indented,
|
|
34
35
|
// the recommended one-liner first. --zai is optional on purpose — no GLM
|
|
@@ -56,7 +57,11 @@ const USAGE = [
|
|
|
56
57
|
' store the dsh launch token as dsh.authToken in',
|
|
57
58
|
' ~/.dsi/settings.yaml, then serve',
|
|
58
59
|
' --dev serve the Vite dev server instead of the built page (takes',
|
|
59
|
-
' port 5175; needs a full checkout: pnpm install)',
|
|
60
|
+
' port 5175; needs a full checkout: pnpm install). Optional',
|
|
61
|
+
' value: --dev in-process boots the DSH host INSIDE the dev',
|
|
62
|
+
' server process (one process, one Ctrl-C; host still on',
|
|
63
|
+
' port 3080; token via in-process intercept). Bare --dev',
|
|
64
|
+
' keeps the two-port pair; --dev pair is the same spelled out.',
|
|
60
65
|
' --sync-kills',
|
|
61
66
|
' kill orphaned dsh/dsi processes left by an earlier run',
|
|
62
67
|
' --ov install the OpenViking memory plugin into the active dsh',
|
|
@@ -195,15 +200,16 @@ if (command === 'dsh') {
|
|
|
195
200
|
// HMR surface as `pnpm dev`, beside the same synced host.
|
|
196
201
|
const rest = process.argv.slice(4);
|
|
197
202
|
const withZai = rest.includes('--zai');
|
|
198
|
-
const withDev = rest.includes('--dev');
|
|
199
203
|
const withOv = rest.includes('--ov');
|
|
200
204
|
if (withZai) addZaiProvider();
|
|
201
205
|
if (withOv) runOvPlugin();
|
|
202
206
|
runSkillsMirror();
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
+
// --dev parse (D4): 'in-process' | 'pair' consumed as a value; bare
|
|
208
|
+
// --dev stays 'pair'. dsi-side flags stripped before the host spawn.
|
|
209
|
+
const { mode, rest: hostArgs0 } = parseDevArg(rest);
|
|
210
|
+
// D7: inspector flags ride the vite child's execArgv, never the host.
|
|
211
|
+
const { rest: hostArgs, execArgv: inspectExecArgv } = extractInspectArgs(hostArgs0);
|
|
212
|
+
runSyncedPair(hostArgs, mode, inspectExecArgv);
|
|
207
213
|
} else if (flag === '--plugin' || flag === '--plugin-restart') {
|
|
208
214
|
// The Plugin Rack ADR (2026-09-27, D3/D5): the dsh CLI is the writer,
|
|
209
215
|
// dsi owns the bounce. Install/remove delegates to the launcher rule
|
|
@@ -240,7 +246,20 @@ if (command === 'dsh') {
|
|
|
240
246
|
// before the marker existed, or from plain `pnpm dev`, carries none.
|
|
241
247
|
// The vite dev process itself is visible to pgrep (unlike the npx
|
|
242
248
|
// shims — different argv rewriting), so ask the process table.
|
|
243
|
-
|
|
249
|
+
// D6 (2026-10-11): the boolean is no longer enough — an in-process
|
|
250
|
+
// floor re-served as 'pair' would double-spawn the host. Read the
|
|
251
|
+
// alive vite's DSI_DEV_MODE via ps -E BEFORE the sweep, resolve the
|
|
252
|
+
// enum, restore exactly what was running.
|
|
253
|
+
const floorEnvDump = readFloorEnv(devFloorPid());
|
|
254
|
+
const mode = resolveBounceMode({
|
|
255
|
+
flagDev: parsed.dev === true,
|
|
256
|
+
devFloorAlive: isDevFloorAlive(),
|
|
257
|
+
floorEnv: floorEnvDump
|
|
258
|
+
});
|
|
259
|
+
// The Faithful Bounce (ADR 2026-10-13, D3): the SAME floor env that
|
|
260
|
+
// records the mode records the inspector flags — re-forward them so
|
|
261
|
+
// the bounce boot comes back debuggable when it left that way.
|
|
262
|
+
const bounceExecArgv = parseInspectArgsFromEnv(floorEnvDump);
|
|
244
263
|
killOrphans();
|
|
245
264
|
// The pkill sweep cannot see every floor shape (RCA 2026-09-27: the
|
|
246
265
|
// npx shims — 'npm exec @deepseek-ai/dsh…' and '.bin/dsh web' — are
|
|
@@ -248,9 +267,11 @@ if (command === 'dsh') {
|
|
|
248
267
|
// 3080 and preflightPort exited 64, killing the detached chain
|
|
249
268
|
// silently). The bounce chain owns the DSI ports by contract, so
|
|
250
269
|
// free them by listener, not by name.
|
|
251
|
-
freeFloorPorts(
|
|
252
|
-
// Restore the SAME UI mode the floor was serving.
|
|
253
|
-
runSyncedPair
|
|
270
|
+
await freeFloorPorts(floorPortSet());
|
|
271
|
+
// Restore the SAME UI mode the floor was serving. The bounce's
|
|
272
|
+
// boolean signal normalizes to 'pair'/'build' inside runSyncedPair;
|
|
273
|
+
// 3.5 teaches the bounce the full enum via DSI_DEV_MODE.
|
|
274
|
+
runSyncedPair([], mode, bounceExecArgv);
|
|
254
275
|
} else if (flag === '--sync-kills') {
|
|
255
276
|
killOrphans();
|
|
256
277
|
process.exit(0);
|
|
@@ -367,56 +388,80 @@ function openBrowser(url, delayMs = 2000) {
|
|
|
367
388
|
* the Vite dev server (`pnpm dev`'s surface) — as the UI. The pair shares
|
|
368
389
|
* one lifecycle — a signal or one child's exit tears down the other, so no
|
|
369
390
|
* orphan survives a Ctrl-C or a crash. */
|
|
370
|
-
async function runSyncedPair(extraArgs, dev =
|
|
391
|
+
async function runSyncedPair(extraArgs, dev = 'build', execArgv = []) {
|
|
392
|
+
// dev accepts the D4 mode enum ('build' | 'pair' | 'in-process'); the
|
|
393
|
+
// legacy boolean true is normalized so older call sites keep working.
|
|
394
|
+
const mode = typeof dev === 'boolean' ? modeFromBoolean(dev) : dev;
|
|
371
395
|
const script = path.join(packageRoot, 'scripts', 'dsh-web-synced.sh');
|
|
372
|
-
|
|
396
|
+
const inProcess = mode === 'in-process';
|
|
397
|
+
if (!inProcess && !existsSync(script)) {
|
|
373
398
|
console.error(`dsi dsh: no sync script at ${script}`);
|
|
374
399
|
process.exit(66);
|
|
375
400
|
}
|
|
376
401
|
// UI port: the built server takes 5174 — the same port `dsi web` uses,
|
|
377
402
|
// so plain `dsi dsh --sync` listens on ONE port. --dev's Vite dev server
|
|
378
403
|
// takes 5175 instead (vite.config.ts pins 5174 strictPort, so pair mode
|
|
379
|
-
// passes 5175 explicitly), giving the two-port dev shape.
|
|
380
|
-
|
|
404
|
+
// passes 5175 explicitly), giving the two-port dev shape. in-process is
|
|
405
|
+
// also a vite floor: same 5175.
|
|
406
|
+
const uiPort = process.env.PORT ?? (mode === 'build' ? '5174' : '5175');
|
|
381
407
|
// The host half takes --port <n> when forwarded (a 3080 held by a
|
|
382
408
|
// dsh web this CLI does not own — e.g. another live conversation
|
|
383
409
|
// floor — is exactly the case the preflight must not kill). The
|
|
384
410
|
// forwarded flag goes to `dsh web` untouched; DSI follows via
|
|
385
411
|
// DSH_BASE_URL so the page dials the host wherever it landed.
|
|
412
|
+
// IN-PROCESS: no host child will be spawned, so preflighting 3080 is
|
|
413
|
+
// pointless — the plugin boots the host on 3080 AFTER vite is up, and
|
|
414
|
+
// a stale 3080 from a foreign floor is that floor's business (the
|
|
415
|
+
// embedded boot fails loud in the plugin, killing nothing).
|
|
386
416
|
warnIfFirstRunDownload();
|
|
387
|
-
|
|
417
|
+
// IN-PROCESS: the plugin reads its port from DSH_HOST_PORT (default 3080)
|
|
418
|
+
// — the UI half MUST dial the same port, so DSH_HOST_PORT participates in
|
|
419
|
+
// the dshBase derivation exactly like a forwarded --port flag. (Smoke
|
|
420
|
+
// bug 2026-10-11: DSH_HOST_PORT=3477 booted the host there while the
|
|
421
|
+
// page dialed 3080 — the production floor — → "Failed to fetch".)
|
|
422
|
+
const pluginPort = inProcess && process.env.DSH_HOST_PORT ? parseInt(process.env.DSH_HOST_PORT, 10) : undefined;
|
|
423
|
+
const hostPort = hostPortFromArgs(extraArgs) ?? pluginPort ?? 3080;
|
|
388
424
|
const dshBase = hostPort === 3080 ? undefined : `http://127.0.0.1:${hostPort}`;
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
425
|
+
if (inProcess) {
|
|
426
|
+
await preflightPort(uiPort, 'DSI page');
|
|
427
|
+
} else {
|
|
428
|
+
await preflightPort(hostPort, 'dsh host');
|
|
429
|
+
await preflightPort(uiPort, 'DSI page');
|
|
430
|
+
}
|
|
431
|
+
// IN-PROCESS: no sync script, no host child — the vite plugin owns the
|
|
432
|
+
// host lifecycle (vite close hook stops it; one Ctrl-C, no orphan).
|
|
433
|
+
const host = inProcess ? null : spawn('/bin/sh', [script, ...extraArgs], {
|
|
395
434
|
stdio: 'inherit',
|
|
396
435
|
env: { ...process.env, DSH_WEB_VERSION: dshWebVersion() }
|
|
397
436
|
});
|
|
398
|
-
const ui =
|
|
437
|
+
const ui = mode === 'build' ? startDsiServer(uiPort, dshBase) : startDsiDevServer(uiPort, dshBase, mode, execArgv);
|
|
399
438
|
// The browser follows the UI: the built page (5174) — or, in --dev mode,
|
|
400
439
|
// the dev server (5175).
|
|
401
440
|
openBrowser(`http://localhost:${uiPort}/`);
|
|
402
441
|
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
403
442
|
process.on(signal, () => {
|
|
404
|
-
host.kill(signal);
|
|
443
|
+
if (host) host.kill(signal); // in-process: no host child — vite close hook stops it
|
|
405
444
|
ui.kill(signal);
|
|
406
445
|
});
|
|
407
446
|
}
|
|
408
447
|
const teardown = (code, signal) => {
|
|
409
|
-
host.kill('SIGTERM');
|
|
448
|
+
if (host) host.kill('SIGTERM');
|
|
410
449
|
ui.kill('SIGTERM');
|
|
411
450
|
process.exit(signal ? 1 : (code ?? 1));
|
|
412
451
|
};
|
|
413
|
-
host.on('exit', teardown);
|
|
452
|
+
if (host) host.on('exit', teardown);
|
|
414
453
|
ui.on('exit', teardown);
|
|
415
454
|
}
|
|
416
455
|
|
|
417
456
|
/** Kill orphaned dsh/dsi leftovers — a pair whose parent died before it
|
|
418
457
|
* could tear down its sibling, or a previous run the terminal lost. */
|
|
419
458
|
function killOrphans() {
|
|
459
|
+
// Test-floor isolation (Port Ledger 34xx/51xx): a scratch floor's bounce
|
|
460
|
+
// must never pkill the production floor's shapes.
|
|
461
|
+
if (process.env.DSI_SKIP_ORPHAN_SWEEP === '1') {
|
|
462
|
+
console.log('dsi dsh: orphan sweep skipped (DSI_SKIP_ORPHAN_SWEEP=1)');
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
420
465
|
for (const pattern of ORPHAN_PATTERNS) {
|
|
421
466
|
try {
|
|
422
467
|
spawnSync('pkill', ['-f', pattern], { stdio: 'pipe' });
|
|
@@ -428,20 +473,49 @@ function killOrphans() {
|
|
|
428
473
|
console.log('dsi dsh: orphan sweep done (dsh-web-synced.sh, dsh bin, dsi server)');
|
|
429
474
|
}
|
|
430
475
|
|
|
431
|
-
/**
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
476
|
+
/** The DSI ports the bounce owns. DSI_FLOOR_PORTS overrides for isolated
|
|
477
|
+
* test floors (Port Ledger: 34xx host, 5180+ UI) so a scratch bounce never
|
|
478
|
+
* touches the production ports. */
|
|
479
|
+
function floorPortSet() {
|
|
480
|
+
const raw = process.env.DSI_FLOOR_PORTS;
|
|
481
|
+
if (!raw) return [3080, 5174, 5175];
|
|
482
|
+
const ports = raw.split(',').map((s) => Number(s.trim())).filter((n) => Number.isInteger(n) && n > 0);
|
|
483
|
+
return ports.length ? ports : [3080, 5174, 5175];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** SIGTERM whatever LISTENS on these ports, then WAIT until they actually
|
|
487
|
+
* free (The Port Takeover ADR 2026-10-12, D2/D3/D5): the fire-and-forget
|
|
488
|
+
* kill of RCA 2026-09-27 raced the successor's bind — the dying in-process
|
|
489
|
+
* host still held 3080 two seconds later and the new floor died on
|
|
490
|
+
* EADDRINUSE (incident: tmp/bounce.log 2026-10-12T03:49:06Z). Every kill
|
|
491
|
+
* and every freed port gets a receipt line; a port still held at the
|
|
492
|
+
* budget exits 64 naming the port and the holder. Only the bounce uses
|
|
493
|
+
* this — the click is the consent (ADR D1); a manual --sync preflight
|
|
494
|
+
* still REFUSES a busy port it does not own. */
|
|
495
|
+
async function freeFloorPorts(ports) {
|
|
496
|
+
const listListeners = (port) => {
|
|
439
497
|
try {
|
|
440
|
-
spawnSync('/bin/sh', ['-c', 'lsof -ti tcp:' + port + ' -sTCP:LISTEN
|
|
498
|
+
const probe = spawnSync('/bin/sh', ['-c', 'lsof -ti tcp:' + port + ' -sTCP:LISTEN 2>/dev/null || true'], { stdio: 'pipe', encoding: 'utf8' });
|
|
499
|
+
return (probe.stdout || '').split('\n').map((s) => Number(s.trim())).filter((n) => Number.isInteger(n) && n > 0);
|
|
441
500
|
} catch {
|
|
442
|
-
// no lsof (or no
|
|
443
|
-
//
|
|
501
|
+
// no lsof (or no sh): nothing to signal; the wait loop below will
|
|
502
|
+
// report the port honestly if it never frees.
|
|
503
|
+
return [];
|
|
444
504
|
}
|
|
505
|
+
};
|
|
506
|
+
const result = await takeoverFloorPorts(ports, {
|
|
507
|
+
listListeners,
|
|
508
|
+
terminate: (pid) => {
|
|
509
|
+
try { process.kill(pid, 'SIGTERM'); } catch { /* already gone — the wait decides */ }
|
|
510
|
+
},
|
|
511
|
+
portFree: (port) => portInUse(port).then((busy) => !busy),
|
|
512
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
513
|
+
now: () => Date.now(),
|
|
514
|
+
log: (line) => console.log(line)
|
|
515
|
+
});
|
|
516
|
+
if (!result.ok) {
|
|
517
|
+
console.error('dsi dsh: port ' + result.port + ' is still in use by a process the takeover does not own (pid ' + result.pid + ') — free it yourself, then re-run');
|
|
518
|
+
process.exit(64);
|
|
445
519
|
}
|
|
446
520
|
}
|
|
447
521
|
|
|
@@ -455,6 +529,28 @@ function isDevFloorAlive() {
|
|
|
455
529
|
return (probe.status ?? 1) === 0;
|
|
456
530
|
}
|
|
457
531
|
|
|
532
|
+
/** The alive dev floor's vite pid (first match), or null. Same match rule
|
|
533
|
+
* as isDevFloorAlive — the pid feeds the D6 mode read (ps -E). */
|
|
534
|
+
function devFloorPid() {
|
|
535
|
+
const probe = spawnSync('pgrep', ['-f', 'deepseek-insight/node_modules/vite/bin/vite.js'], { stdio: 'pipe' });
|
|
536
|
+
if ((probe.status ?? 1) !== 0) return null;
|
|
537
|
+
const first = (probe.stdout?.toString() || '').split('\n').find(Boolean);
|
|
538
|
+
return first ? first.trim() : null;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** The floor's DSI_DEV_MODE via ps -E (macOS: own processes only). Returns
|
|
542
|
+
* '' when the read fails for any reason — resolveBounceMode then falls back
|
|
543
|
+
* to the boolean arms, the pre-D6 behavior, never a wrong restore. */
|
|
544
|
+
function readFloorEnv(pid) {
|
|
545
|
+
if (!pid) return '';
|
|
546
|
+
try {
|
|
547
|
+
const ps = spawnSync('ps', ['-E', '-p', String(pid)], { stdio: 'pipe', timeout: 5000 });
|
|
548
|
+
return ps.status === 0 ? ps.stdout?.toString() || '' : '';
|
|
549
|
+
} catch {
|
|
550
|
+
return '';
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
458
554
|
/** Add the ZAI provider and default model to ~/.dsh/settings.yaml when
|
|
459
555
|
* absent. Existing entries always win — an already-configured host is
|
|
460
556
|
* rewritten only when at least one block was added. Exits non-zero when
|
|
@@ -573,7 +669,7 @@ function runSkillsMirror() {
|
|
|
573
669
|
* sources and devDependencies exist — a published tarball (bin + build)
|
|
574
670
|
* refuses loudly instead of half-booting. The --port CLI flag overrides the
|
|
575
671
|
* config's server.port while strictPort keeps the fail-loud contract. */
|
|
576
|
-
function startDsiDevServer(defaultPort = '5175', dshBase = undefined) {
|
|
672
|
+
function startDsiDevServer(defaultPort = '5175', dshBase = undefined, mode = 'pair', execArgv = []) {
|
|
577
673
|
const viteBin = path.join(packageRoot, 'node_modules', 'vite', 'bin', 'vite.js');
|
|
578
674
|
if (!existsSync(viteBin)) {
|
|
579
675
|
console.error(`dsi dsh --dev: no vite at ${viteBin} — --dev needs a full DSI checkout (pnpm install); use the built server instead`);
|
|
@@ -581,7 +677,13 @@ function startDsiDevServer(defaultPort = '5175', dshBase = undefined) {
|
|
|
581
677
|
}
|
|
582
678
|
// --host matches the built server's 127.0.0.1 bind: vite's default
|
|
583
679
|
// 'localhost' can resolve ::1-only and strand a 127.0.0.1 client.
|
|
584
|
-
return spawn(process.execPath, [viteBin, 'dev', '--host', process.env.HOST ?? '127.0.0.1', '--port', process.env.PORT ?? defaultPort], {
|
|
680
|
+
return spawn(process.execPath, [...execArgv, viteBin, 'dev', '--host', process.env.HOST ?? '127.0.0.1', '--port', process.env.PORT ?? defaultPort], {
|
|
681
|
+
// D7: inspector flags lead the child argv — node consumes options before
|
|
682
|
+
// the script path, so `node --inspect vite.js dev` debugs THIS process
|
|
683
|
+
// (vite + the embedded DSH host share one heap). The spawn execArgv
|
|
684
|
+
// OPTION was tried first and empirically never activated the inspector
|
|
685
|
+
// (silent no-op, probed 2026-10-11); leading args are the reliable form.
|
|
686
|
+
// NODE_OPTIONS is banned — it leaks to every child and collides on port.
|
|
585
687
|
stdio: 'inherit',
|
|
586
688
|
cwd: packageRoot,
|
|
587
689
|
env: {
|
|
@@ -592,6 +694,13 @@ function startDsiDevServer(defaultPort = '5175', dshBase = undefined) {
|
|
|
592
694
|
// this from the API server's env to re-serve --dev instead of the
|
|
593
695
|
// stale build. Nothing else may set it.
|
|
594
696
|
DSI_DEV: '1',
|
|
697
|
+
// D4/D6: the mode rides as env, never past the CLI to the host.
|
|
698
|
+
...devModeEnv(mode),
|
|
699
|
+
// The Faithful Bounce (ADR 2026-10-13, D1): the inspector flags are
|
|
700
|
+
// part of the launch record — stamped so every restorer (the bounce
|
|
701
|
+
// boot via ps -E, the delayed re-serve via inherited env) can put
|
|
702
|
+
// them back exactly as launched.
|
|
703
|
+
...(execArgv.length ? { DSI_INSPECT_ARGS: execArgv.join(' ') } : {}),
|
|
595
704
|
...(dshBase !== undefined ? { DSH_BASE_URL: dshBase } : {})
|
|
596
705
|
}
|
|
597
706
|
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dev-mode — parse the --dev flag's optional value and derive env markers.
|
|
3
|
+
*
|
|
4
|
+
* ADR: dev/architectural-decission/2026-10/2026-10-11 - The In-Process Host
|
|
5
|
+
* (D4: --dev gains an optional value; bare --dev keeps today's meaning;
|
|
6
|
+
* the mode rides to vite via env, never past the CLI to the dsh web host).
|
|
7
|
+
*
|
|
8
|
+
* Modes:
|
|
9
|
+
* 'build' — the built page (no --dev)
|
|
10
|
+
* 'pair' — vite dev server + separate host child (bare --dev / --dev pair)
|
|
11
|
+
* 'in-process' — vite dev server boots the host itself (--dev in-process)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Tokens the --dev flag may consume as its value. Anything else after
|
|
15
|
+
* --dev belongs to some other flag (or the host) and is left alone. */
|
|
16
|
+
const DEV_VALUES = new Set(['in-process', 'pair']);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse a rest-args array for --dev (and strip dsi-side flags).
|
|
20
|
+
* @param {string[]} rest — args AFTER the subcommand (e.g. after 'dsh --sync')
|
|
21
|
+
* @returns {{ mode: 'build'|'pair'|'in-process', rest: string[] }}
|
|
22
|
+
* rest — args with --dev (and its value) plus other dsi-side flags removed,
|
|
23
|
+
* safe to forward to the dsh web host.
|
|
24
|
+
*/
|
|
25
|
+
export function parseDevArg(rest) {
|
|
26
|
+
const out = [];
|
|
27
|
+
let mode = 'build';
|
|
28
|
+
for (let i = 0; i < rest.length; i++) {
|
|
29
|
+
const arg = rest[i];
|
|
30
|
+
if (arg === '--zai' || arg === '--ov') continue; // dsi-side, stripped (pre-existing contract)
|
|
31
|
+
if (arg === '--dev') {
|
|
32
|
+
const next = rest[i + 1];
|
|
33
|
+
if (next && DEV_VALUES.has(next)) {
|
|
34
|
+
mode = next === 'pair' ? 'pair' : 'in-process';
|
|
35
|
+
i++; // consume the value — it never reaches the host
|
|
36
|
+
} else {
|
|
37
|
+
mode = 'pair'; // bare --dev keeps today's meaning (D4)
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
out.push(arg);
|
|
42
|
+
}
|
|
43
|
+
return { mode, rest: out };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Env additions for the vite child expressing the chosen mode.
|
|
48
|
+
* DSI_DEV=1 is the pre-existing dev-floor marker (RCA 2026-09-27) — kept for
|
|
49
|
+
* the bounce chain's boolean probe; DSI_DEV_MODE is the D6 mode enum.
|
|
50
|
+
*/
|
|
51
|
+
export function devModeEnv(mode) {
|
|
52
|
+
if (mode === 'build') return {};
|
|
53
|
+
const env = { DSI_DEV: '1', DSI_DEV_MODE: mode };
|
|
54
|
+
if (mode === 'in-process') env.DSI_DEV_IN_PROCESS = '1';
|
|
55
|
+
return env;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Map the legacy boolean dev signal (bounceDevMode's return, older floors)
|
|
60
|
+
* to a mode. Wave 3.5 will teach the bounce the enum; until then the bounce
|
|
61
|
+
* can only ever restore 'pair' or 'build'.
|
|
62
|
+
*/
|
|
63
|
+
export function modeFromBoolean(dev) {
|
|
64
|
+
return dev ? 'pair' : 'build';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Parse ps -E -p <pid> output for the floor's DSI_DEV_MODE marker (D6).
|
|
69
|
+
* Pure: the ps invocation is injected by dsi.mjs.
|
|
70
|
+
* @param {string} psOutput — full environment dump of the vite process
|
|
71
|
+
* @returns {'in-process' | null} null = marker absent (pair/build floor)
|
|
72
|
+
*/
|
|
73
|
+
export function parseDevModeFromPsE(psOutput) {
|
|
74
|
+
const m = /(?:^|\s)DSI_DEV_MODE=(\S+)/.exec(psOutput || '');
|
|
75
|
+
return m && m[1] === 'in-process' ? 'in-process' : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolve the mode a bounce must RESTORE (D6 — the plugin-apply restart
|
|
80
|
+
* runs in a fresh CLI that cannot see the original argv).
|
|
81
|
+
*
|
|
82
|
+
* Priority:
|
|
83
|
+
* 1. the alive floor's own env says in-process → restore 'in-process'
|
|
84
|
+
* (D6, sharpened 2026-10-12 Port Takeover: the floor's RECORDED mode is
|
|
85
|
+
* authoritative over the bounce's --dev flag — the verb's chain always
|
|
86
|
+
* carries --dev, and letting it win bounced in-process floors into
|
|
87
|
+
* 'pair', which spawned a sync-script host AND left the stale
|
|
88
|
+
* DSI_DEV_IN_PROCESS marker active in the chain env, so two hosts
|
|
89
|
+
* fought for 3080 and the floor died on EADDRINUSE every click)
|
|
90
|
+
* 2. explicit --dev flag on the bounce itself → 'pair' (legacy semantics;
|
|
91
|
+
* the operator asked for a dev re-serve)
|
|
92
|
+
* 3. a dev floor is alive (boolean probe) → 'pair'
|
|
93
|
+
* 4. nothing alive → 'build'
|
|
94
|
+
* @param {{ flagDev?: boolean, devFloorAlive?: boolean, floorEnv?: string }} p
|
|
95
|
+
*/
|
|
96
|
+
export function resolveBounceMode(p = {}) {
|
|
97
|
+
if (p.floorEnv && parseDevModeFromPsE(p.floorEnv) === 'in-process') return 'in-process';
|
|
98
|
+
if (p.flagDev) return 'pair';
|
|
99
|
+
if (p.devFloorAlive) return 'pair';
|
|
100
|
+
return 'build';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Parse a ps -E environment dump for DSI_INSPECT_ARGS (ADR 2026-10-13
|
|
105
|
+
* "The Faithful Bounce", D3): the space-joined inspector flags the launcher
|
|
106
|
+
* stamped beside the dev-mode markers. Pure: the dump string is injected.
|
|
107
|
+
* @param {string} psOutput — full environment dump of the vite process
|
|
108
|
+
* @returns {string[]} inspector argv ([] = none recorded / pre-stamp floor)
|
|
109
|
+
*/
|
|
110
|
+
export function parseInspectArgsFromEnv(psOutput) {
|
|
111
|
+
// ps -E is ONE line of KEY=VALUE tokens — the value ends at the next
|
|
112
|
+
// KEY= token (identifier-shaped, so '--inspect=...' never trips it),
|
|
113
|
+
// at a newline, or at end-of-dump. Lazy capture + lookahead.
|
|
114
|
+
const m = /(?:^|\s)DSI_INSPECT_ARGS=(.*?)(?=\s+[A-Za-z_][A-Za-z0-9_]*=|\s*$|\n|\r)/.exec(psOutput || '');
|
|
115
|
+
if (!m) return [];
|
|
116
|
+
return m[1].trim().split(/\s+/).filter(Boolean);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Pull Node inspector flags out of the rest-args: --inspect, --inspect-brk,
|
|
121
|
+
* --inspect=<host:port>. Forwarded to the vite child's execArgv (D7) —
|
|
122
|
+
* NEVER via NODE_OPTIONS, which would leak the inspector to every child
|
|
123
|
+
* dsi.mjs spawns and collide on the default port.
|
|
124
|
+
* @param {string[]} rest
|
|
125
|
+
* @returns {{ rest: string[], execArgv: string[] }}
|
|
126
|
+
*/
|
|
127
|
+
export function extractInspectArgs(rest) {
|
|
128
|
+
const out = [];
|
|
129
|
+
const execArgv = [];
|
|
130
|
+
for (const arg of rest) {
|
|
131
|
+
if (arg === '--inspect' || arg === '--inspect-brk' || arg.startsWith('--inspect=')) {
|
|
132
|
+
execArgv.push(arg);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
out.push(arg);
|
|
136
|
+
}
|
|
137
|
+
return { rest: out, execArgv };
|
|
138
|
+
}
|
package/bin/lib/plugin-chain.mjs
CHANGED
|
@@ -70,6 +70,53 @@ export function bounceDevMode(flagDev, devFloorAlive) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/** The takeover's bounded-wait budget and poll cadence (The Port Takeover
|
|
74
|
+
* ADR 2026-10-12, D3): after the SIGTERM sweep the chain waits until every
|
|
75
|
+
* DSI port actually frees — at most this budget — then re-serves. A port
|
|
76
|
+
* still held at the deadline is an honest exit 64, never a half-boot. */
|
|
77
|
+
export const TAKEOVER_BUDGET_MS = 10_000;
|
|
78
|
+
export const TAKEOVER_POLL_MS = 250;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The takeover sweep (The Port Takeover ADR 2026-10-12, D2/D3/D5): SIGTERM
|
|
82
|
+
* whatever LISTENS on the given ports, then WAIT until each port frees,
|
|
83
|
+
* bounded. Pure orchestration — every effect is injected, so unit tests run
|
|
84
|
+
* it against fakes and the real half stays in dsi.mjs.
|
|
85
|
+
*
|
|
86
|
+
* @param {number[]} ports
|
|
87
|
+
* @param {{ listListeners: (port: number) => number[], terminate: (pid: number) => void,
|
|
88
|
+
* portFree: (port: number) => boolean | Promise<boolean>, sleep: (ms: number) => Promise<void>,
|
|
89
|
+
* now: () => number, log: (line: string) => void }} effects
|
|
90
|
+
* @returns {Promise<{ ok: boolean, port?: number, pid?: number | string }>} ok:false names the port and holder (exit 64 is the caller's)
|
|
91
|
+
*/
|
|
92
|
+
export async function takeoverFloorPorts(ports, effects) {
|
|
93
|
+
const { listListeners, terminate, portFree, sleep, now, log } = effects;
|
|
94
|
+
const start = now();
|
|
95
|
+
for (const port of ports) {
|
|
96
|
+
for (const pid of listListeners(port)) {
|
|
97
|
+
terminate(pid);
|
|
98
|
+
log('takeover: SIGTERM pid ' + pid + ' on port ' + port);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const port of ports) {
|
|
102
|
+
let freed = null;
|
|
103
|
+
while (now() - start <= TAKEOVER_BUDGET_MS) {
|
|
104
|
+
if (await portFree(port)) {
|
|
105
|
+
freed = now() - start;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
await sleep(TAKEOVER_POLL_MS);
|
|
109
|
+
}
|
|
110
|
+
if (freed === null) {
|
|
111
|
+
const holder = listListeners(port)[0] ?? 'unknown';
|
|
112
|
+
log('takeover: port ' + port + ' still held by pid ' + holder + ' after ' + TAKEOVER_BUDGET_MS + 'ms — giving up');
|
|
113
|
+
return { ok: false, port, pid: holder };
|
|
114
|
+
}
|
|
115
|
+
log('takeover: port ' + port + ' free after ' + freed + 'ms');
|
|
116
|
+
}
|
|
117
|
+
return { ok: true };
|
|
118
|
+
}
|
|
119
|
+
|
|
73
120
|
/**
|
|
74
121
|
* The dsh launcher rule (runOvPlugin's, generalized): DSH_WEB_CMD override,
|
|
75
122
|
* else the pinned published dsh via npx. The webVersion pin comes from the
|