@deepseek-insight/dsi 0.6.9 → 0.6.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/dsi-ov-setup/SKILL.md +134 -0
- package/.agents/skills/dsi-plugin-rack/rack.d.mts +35 -0
- package/.agents/skills/dsi-plugin-rack/rack.mjs +168 -26
- package/build/client/_app/immutable/assets/{2.BGqV97Jd.css → 2.BuaCdYrs.css} +1 -1
- package/build/client/_app/immutable/chunks/{UkmEG9Ws.js → BNTNz9oR.js} +1 -1
- package/build/client/_app/immutable/chunks/CiFavIif.js +1 -0
- package/build/client/_app/immutable/chunks/DN37js12.js +1 -0
- package/build/client/_app/immutable/chunks/KLsbJ_6V.js +2 -0
- package/build/client/_app/immutable/entry/{app.e7xoYywe.js → app.BNlAGT8a.js} +2 -2
- package/build/client/_app/immutable/entry/start.BQEoVg3q.js +1 -0
- package/build/client/_app/immutable/nodes/{1.E4wiL8sP.js → 1.CeFdv8vr.js} +1 -1
- package/build/client/_app/immutable/nodes/{2.BBV-AGuB.js → 2.Boib-Q5k.js} +195 -195
- 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/{engine.js-CrdWRSwf.js → engine.js-1gYqtdBP.js} +17 -2
- package/build/server/chunks/chunks/{internal.js-BnJXHc1S.js → internal.js-XBbRSE8G.js} +2 -2
- package/build/server/chunks/chunks/{state.js-CAjHzrRZ.js → state.js-DgcRLBLq.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/plugins/apply/{_server.ts.js-CpO--Jwf.js → _server.ts.js-BNEUWbaf.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/plugins/reload/{_server.ts.js-BACFXWMj.js → _server.ts.js-Ct28d7_t.js} +2 -2
- package/build/server/chunks/entries/endpoints/api/plugins/snapshot/{_server.ts.js-BJg0hLlC.js → _server.ts.js-DswGIO4S.js} +2 -2
- package/build/server/chunks/entries/pages/{_error.svelte.js-BXeIuPj-.js → _error.svelte.js-CRYTX4NJ.js} +3 -3
- package/build/server/chunks/entries/pages/{_page.svelte.js-CW-EdJ00.js → _page.svelte.js-jCShpNgz.js} +164 -14
- package/build/server/chunks/{handler-DimUSmTW.js → handler-CDSfkiHz.js} +3 -3
- package/build/server/chunks/{index.js-BQUp4Xgn.js → index.js-BOeQQ7jF.js} +2 -2
- package/build/server/chunks/{manifest.js-BWy6zgHL.js → manifest.js-CvDWy0oM.js} +7 -7
- package/build/server/chunks/nodes/1.js-1rT6o2jA.js +9 -0
- package/build/server/chunks/nodes/{2.js-CzgP_zp4.js → 2.js-CGXIkDa5.js} +4 -4
- package/package.json +1 -1
- package/skills/dsi-add-plugin/SKILL.md +53 -0
- package/skills/dsi-add-plugin/references/allowbuilds-template.md +55 -0
- package/skills/dsi-add-plugin/references/checklist.md +98 -0
- package/skills/dsi-add-plugin/references/verification.md +67 -0
- package/template/.dsi/prompts.sqlite +0 -0
- package/template/.dsi/resources/plugins-reff.md +4 -2
- package/build/client/_app/immutable/chunks/C1F3aCiz.js +0 -2
- package/build/client/_app/immutable/chunks/CmnxnK6v.js +0 -1
- package/build/client/_app/immutable/chunks/jugX_o5s.js +0 -1
- package/build/client/_app/immutable/entry/start.CnkkMjJa.js +0 -1
- package/build/server/chunks/nodes/1.js-DJ3dsgdq.js +0 -9
- package/template/dsh-app-dev-preset/README.md +0 -69
- package/template/dsh-app-dev-preset/cordis.patch.yml +0 -219
- package/template/dsh-app-dev-preset/package.json +0 -7
- package/template/dsh-research-preset/README.md +0 -57
- package/template/dsh-research-preset/cordis.patch.yml +0 -118
- package/template/dsh-research-preset/package.json +0 -7
|
@@ -131,6 +131,140 @@ This is the only install path that reconciles `dsh.profile.bundles` and survives
|
|
|
131
131
|
- Two config files, one directory (§1) — check the right one for the symptom.
|
|
132
132
|
- `auth_mode: "dev"` = no auth, localhost only. Never bind `0.0.0.0` without switching to API-key auth (docs: Authentication guide).
|
|
133
133
|
- Piped `init` answers are prompt-order-fragile (Step 3) — count prompts, don't trust defaults.
|
|
134
|
+
---
|
|
135
|
+
name: dsi-ov-setup
|
|
136
|
+
description: Use when OpenViking memory is broken or missing in DeepSeek Harness — server disconnected, server not installed, ov.conf missing, plugin not loaded, or a fresh machine needs the whole OpenViking + DSH memory integration set up (plugin, skill, server). Covers diagnosis, install, config, start, and verification.
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
# OpenViking + DSH Setup and Recovery
|
|
140
|
+
|
|
141
|
+
Companion KB: `dev/kb/setup/2026-09-16 - OpenViking Server + DSH Memory Plugin — From Cold Start to Recalled Memory.md` — read it for the why behind every step. This skill is the do-path.
|
|
142
|
+
|
|
143
|
+
## 0. Diagnose first — which failure is this?
|
|
144
|
+
|
|
145
|
+
Run in order; stop at the first failing gate:
|
|
146
|
+
|
|
147
|
+
| Gate | Command | Healthy looks like |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| Server process | `curl -s --max-time 3 http://127.0.0.1:1933/health` | `{"status":"ok","healthy":true,...}` |
|
|
150
|
+
| Server config | `ls ~/.openviking/ov.conf` | exists |
|
|
151
|
+
| Client config | `cat ~/.openviking/ovcli.conf` | `url` = `http://127.0.0.1:1933` |
|
|
152
|
+
| Package | `openviking-server --version` | prints a version |
|
|
153
|
+
| Plugin installed | `grep @openviking ~/.dsh/profiles/web/package.json` + `ls ~/.dsh/profiles/web/node_modules/@openviking/dsh-memory-plugin` | both the bundle entry and the package exist |
|
|
154
|
+
| MCP connection | harness tool list shows `mcp__openviking__*` | tools present |
|
|
155
|
+
|
|
156
|
+
- Health fails + `ov.conf` missing → **Recovery path (§2)**, the common case.
|
|
157
|
+
- Health fails + `ov.conf` exists → run `openviking-server doctor`, fix what it names, go to §2 step 4.
|
|
158
|
+
- Plugin missing from manifest or `node_modules` → reinstall via the sanctioned path (§2 step 6): from the running host's checkout, `pnpm dsh plugin --profile web add @openviking/dsh-memory-plugin`. This persists both the dependency and the `dsh.profile.bundles` entry, so profile rebuilds keep it. A bare `pnpm install` in the profile dir is a dead end — bundles are not `dependencies` and pnpm reports "Already up to date" while the plugin stays missing (RCA 2026-09-18 — The Vanished Plugin).
|
|
159
|
+
- Health ok but MCP disconnected → the plugin reconnects on its own within a turn or two; if not after a full turn, restart the harness session. Do not reinstall anything.
|
|
160
|
+
- Package missing → fresh install: do §2 steps 1–6 from scratch.
|
|
161
|
+
|
|
162
|
+
## 1. Facts to keep straight (most-confused pair)
|
|
163
|
+
|
|
164
|
+
- `~/.openviking/ovcli.conf` — **client** pointer: `{"url": "http://127.0.0.1:1933", "api_key": ""}`. Missing it breaks CLI commands only.
|
|
165
|
+
- `~/.openviking/ov.conf` — **server** model config (embedding + VLM). Missing it means the server process cannot start at all. This is the usual blocker.
|
|
166
|
+
- The DSH plugin (`@openviking/dsh-memory-plugin`, in the profile bundles at `~/.dsh/profiles/web/package.json`) is a **client only** — it never starts the server, and it reconnects automatically once the server is healthy. Never "fix" a disconnected MCP by touching the plugin.
|
|
167
|
+
- `openviking`/`ov` = CLI/SDK; `openviking-server` = the daemon. Same pip package.
|
|
168
|
+
|
|
169
|
+
## 2. Recovery / install path
|
|
170
|
+
|
|
171
|
+
### Step 1 — Install the package (skip if `openviking-server --version` works)
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
pip install openviking --upgrade --force-reinstall
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Entry points land in `~/.local/bin`: `ov`, `openviking`, `openviking-server`, `vikingbot`.
|
|
178
|
+
|
|
179
|
+
### Step 2 — CLI housekeeping
|
|
180
|
+
|
|
181
|
+
```sh
|
|
182
|
+
ov language en # v0.3.23+ refuses most commands without this; writes ovcli.settings.conf
|
|
183
|
+
ov config add custom --name local --url http://127.0.0.1:1933 --activate -o json # only if ovcli.conf missing
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Note: both write under `~/.openviking/` — they fail under a workspace-only file sandbox; run with full file access.
|
|
187
|
+
|
|
188
|
+
### Step 3 — Create the server config: `openviking-server init`
|
|
189
|
+
|
|
190
|
+
Preferred: run it interactively and let the human answer. If driving it with piped answers, the wizard's prompt order is exact and hidden prompts exist — the tested sequence for the recommended local setup:
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
1 # mode: step-by-step
|
|
194
|
+
3 # embedding: lightweight CPU (llama.cpp, bge-small-zh-v1.5-f16, no key)
|
|
195
|
+
1 # embedding model: (only option — EASY TO MISS; missing this shifts every later answer)
|
|
196
|
+
6 # VLM: GLM (subscription API key)
|
|
197
|
+
2 # GLM plan: coding
|
|
198
|
+
<key> # API key — pass via stdin from an env var; NEVER in the command line or logs
|
|
199
|
+
N # query planner via Ollama: no
|
|
200
|
+
1 # bind 127.0.0.1 (dev, no auth)
|
|
201
|
+
1933 # port
|
|
202
|
+
Y # save
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
After saving the wizard echoes the resolved API key in its summary — **redact any captured log** before storing it.
|
|
206
|
+
|
|
207
|
+
Decision guardrails (from the KB §4): embedding `[3]` CPU is the default because it is keyless and tiny; offer `[1]` cloud embedding only when the user cares about retrieval quality and has a key; VLM is the piece worth a strong model — GLM via an existing subscription beats skipping it. `[9]` skip-VLM gives a server that stores but extracts poorly.
|
|
208
|
+
|
|
209
|
+
### Step 4 — Wait out the build
|
|
210
|
+
|
|
211
|
+
Choosing CPU embedding triggers `pip install "openviking[local-embed]"`, which **builds `llama-cpp-python` from source** — several minutes of silent wheel-building. It is not hung. Do not kill it; verify with `ps aux | grep "openviking-server init"`.
|
|
212
|
+
|
|
213
|
+
### Step 5 — Validate and start
|
|
214
|
+
|
|
215
|
+
```sh
|
|
216
|
+
openviking-server doctor # expect: all checks PASS (config, auth, disk ≥ several GB)
|
|
217
|
+
openviking-server # foreground proof; then move to a supervisor
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Sandbox warning: server startup acquires a PID lock at
|
|
221
|
+
`~/.openviking/data/.openviking.pid` — under a workspace-only file sandbox it
|
|
222
|
+
dies with `PermissionError` / exit 3 at once. Start the server with full file
|
|
223
|
+
access (same as step 2's CLI housekeeping).
|
|
224
|
+
|
|
225
|
+
For persistence: launchd/systemd — never rely on an agent shell outliving the
|
|
226
|
+
session. In a harness session, never start the daemon with `nohup … &` inside a
|
|
227
|
+
**foreground** bash call: the harness reaps the call's process group when the
|
|
228
|
+
call returns and the daemon dies seconds later, health-ok inside the call but
|
|
229
|
+
connection-refused on the next one (RCA 2026-09-21 — The Server That Slept).
|
|
230
|
+
Run it as a harness background job instead. Verify:
|
|
231
|
+
|
|
232
|
+
```sh
|
|
233
|
+
curl -s http://127.0.0.1:1933/health # {"status":"ok","healthy":true,...}
|
|
234
|
+
ov status # "Connected (Healthy)", queue 0 pending / 0 errors
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
A stale `~/.openviking/pending/` queue (hundreds of files) from the downtime drains on its own — do not delete it.
|
|
238
|
+
|
|
239
|
+
### Step 6 — Confirm the integration
|
|
240
|
+
|
|
241
|
+
Within a turn of the server going healthy, the plugin reconnects (MCP tools answer, `<openviking-context>` appears at the next session start). If MCP tools still error after a full turn: restart the harness session once. Only then suspect the bundle (`~/.dsh/profiles/web/package.json` must list `@openviking/dsh-memory-plugin` in `dsh.profile.bundles`).
|
|
242
|
+
|
|
243
|
+
If the plugin must be (re)installed, run from the **running host's checkout** (find it: `lsof -p <dsh-web-pid> | grep cwd`):
|
|
244
|
+
|
|
245
|
+
```sh
|
|
246
|
+
pnpm dsh plugin --profile web add @openviking/dsh-memory-plugin
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
This is the only install path that reconciles `dsh.profile.bundles` and survives profile rebuilds. With `patchReload: "live"` in the manifest the profile recomposes without a host restart and `<openviking-context>` returns mid-session.
|
|
250
|
+
|
|
251
|
+
## 3. Verification checklist (all seven, in order)
|
|
252
|
+
|
|
253
|
+
1. `openviking-server --version` → prints version
|
|
254
|
+
2. `ls ~/.openviking/ov.conf ~/.openviking/ovcli.conf` → both exist
|
|
255
|
+
3. `openviking-server doctor` → all PASS
|
|
256
|
+
4. `curl -s http://127.0.0.1:1933/health` → `"healthy":true`
|
|
257
|
+
5. `ov status` → Connected (Healthy), 0 errors
|
|
258
|
+
6. `ov find "smoke test"` after any resource exists → ranked hits, not a connection error
|
|
259
|
+
7. New session → `<openviking-context>` block present
|
|
260
|
+
|
|
261
|
+
## 4. Known traps
|
|
262
|
+
|
|
263
|
+
- `ov status` saying "Unreachable" while the server is fine → the language gate; run `ov language en` first.
|
|
264
|
+
- Two config files, one directory (§1) — check the right one for the symptom.
|
|
265
|
+
- `auth_mode: "dev"` = no auth, localhost only. Never bind `0.0.0.0` without switching to API-key auth (docs: Authentication guide).
|
|
266
|
+
- Piped `init` answers are prompt-order-fragile (Step 3) — count prompts, don't trust defaults.
|
|
267
|
+
- When a D4 peer-dependency rejection forces `dsh plugin allow-version`, allow the **exact** plugin version and then install by **exact pin** (`add pkg@0.5.16`, not a `^` range) — a caret range can resolve to a sibling version the exemption does not cover, and the install rolls back (RCA 2026-09-24).
|
|
134
268
|
- Re-examine actual prompt output on every version bump; this sequence was tested against OpenViking 0.4.20 / plugin 0.3.2 (2026-09-16) and the wizard's prompts are not a stable contract.
|
|
135
269
|
|
|
136
270
|
## 5. Report the run — always, before finishing
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Type surface of the rack engine (rack.mjs) for test files. */
|
|
2
|
+
declare const CACHE_VERSION: 2;
|
|
3
|
+
export interface RackRow {
|
|
4
|
+
n: string;
|
|
5
|
+
id: string;
|
|
6
|
+
group?: 'owned' | 'external';
|
|
7
|
+
repo: string;
|
|
8
|
+
author?: string | null;
|
|
9
|
+
authorUrl?: string | null;
|
|
10
|
+
version?: string | null;
|
|
11
|
+
description?: string | null;
|
|
12
|
+
order?: number;
|
|
13
|
+
installSpec?: string;
|
|
14
|
+
installed: boolean;
|
|
15
|
+
pkg: string | null;
|
|
16
|
+
bundle: string | null;
|
|
17
|
+
}
|
|
18
|
+
export interface GardenRow {
|
|
19
|
+
id: string;
|
|
20
|
+
group: 'owned';
|
|
21
|
+
version: string | null;
|
|
22
|
+
description: string | null;
|
|
23
|
+
order: number;
|
|
24
|
+
installSpec: string;
|
|
25
|
+
repo: string;
|
|
26
|
+
}
|
|
27
|
+
export function scanGarden(gardenPath: string | undefined | null): GardenRow[];
|
|
28
|
+
export function parseReff(text: string): { plugins: { id: string; repo: string; author: string | null; authorUrl: string | null }[]; warnings: { code: string; detail: string }[] };
|
|
29
|
+
export function findDepKey(manifest: { deps: Record<string, string> }, id: string, repo?: string): string | null;
|
|
30
|
+
export function readManifest(path: string): { deps: Record<string, string>; bundles: string[]; missing: boolean };
|
|
31
|
+
export function isInstalled(manifest: { deps: Record<string, string> }, id: string, repo?: string): boolean;
|
|
32
|
+
export function buildSnapshot(args: { reffPath?: string; manifestPath?: string; gardenPath?: string; profile?: string }): { snapshot: { v: number; generatedAt: string; profile: string; plugins: RackRow[] }; warnings: { code: string; detail: string }[] };
|
|
33
|
+
export function toInstallSpec(repo: string): string;
|
|
34
|
+
export function installOwned(manifestPath: string, row: { id: string; installSpec: string }): string;
|
|
35
|
+
export function restoreClobberedBundles(manifestPath: string, preOwnedRows: { id: string; installSpec?: string; pkg?: string | null }[]): boolean;
|
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
* dependencies[id] AND dsh.profile.bundles includes id. Gates surface, they
|
|
15
15
|
* never auto-clear (D6).
|
|
16
16
|
*/
|
|
17
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
18
18
|
import { homedir } from 'node:os';
|
|
19
19
|
import { dirname, join, resolve } from 'node:path';
|
|
20
20
|
import { spawnSync } from 'node:child_process';
|
|
21
21
|
|
|
22
|
-
const CACHE_VERSION =
|
|
22
|
+
export const CACHE_VERSION = 2;
|
|
23
23
|
|
|
24
24
|
function parseArgs(argv) {
|
|
25
25
|
const out = { command: argv[0], reload: false, reff: null, cache: null, manifest: null, profile: process.env.DSH_PROFILE ?? 'web', dshCmd: null, targets: [] };
|
|
@@ -30,6 +30,8 @@ function parseArgs(argv) {
|
|
|
30
30
|
else if (a === '--cache') out.cache = argv[++i];
|
|
31
31
|
else if (a === '--manifest') out.manifest = argv[++i];
|
|
32
32
|
else if (a === '--profile') out.profile = argv[++i];
|
|
33
|
+
else if (a === '--garden') out.garden = argv[++i];
|
|
34
|
+
else if (a === '--garden-repo') out.gardenRepo = argv[++i];
|
|
33
35
|
else if (a === '--dsh-cmd') out.dshCmd = argv[++i];
|
|
34
36
|
else out.targets.push(a);
|
|
35
37
|
}
|
|
@@ -54,6 +56,58 @@ export function parseReff(text) {
|
|
|
54
56
|
return { plugins, warnings };
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
|
|
60
|
+
/** Owned-species enumeration (ADR "The Plugin Garden" 2026-10-07, D1/D2):
|
|
61
|
+
* every direct child of the garden dir with a package.json wiring
|
|
62
|
+
* dsh.bundle.patch is one owned rack row, identified by its OWN package
|
|
63
|
+
* name. Rows carry version/description from the same file and an optional
|
|
64
|
+
* dsh.rack.order for display sort. Returns [] for a missing garden. */
|
|
65
|
+
export function scanGarden(gardenPath) {
|
|
66
|
+
if (!gardenPath || !existsSync(gardenPath)) return [];
|
|
67
|
+
const rows = [];
|
|
68
|
+
for (const child of readdirSync(gardenPath, { withFileTypes: true })) {
|
|
69
|
+
if (!child.isDirectory()) continue;
|
|
70
|
+
const pkgPath = join(gardenPath, child.name, 'package.json');
|
|
71
|
+
if (!existsSync(pkgPath)) continue;
|
|
72
|
+
let pkg;
|
|
73
|
+
try { pkg = JSON.parse(readFileSync(pkgPath, 'utf8')); } catch { continue; }
|
|
74
|
+
const patch = pkg && pkg.dsh && pkg.dsh.bundle && pkg.dsh.bundle.patch;
|
|
75
|
+
if (!patch) continue;
|
|
76
|
+
const order = (pkg.dsh && pkg.dsh.rack && pkg.dsh.rack.order) ?? Number.POSITIVE_INFINITY;
|
|
77
|
+
rows.push({
|
|
78
|
+
id: pkg.name,
|
|
79
|
+
group: 'owned',
|
|
80
|
+
version: pkg.version ?? null,
|
|
81
|
+
description: pkg.description ?? null,
|
|
82
|
+
order,
|
|
83
|
+
installSpec: 'link:' + join(gardenPath, child.name),
|
|
84
|
+
repo: 'link:' + join(gardenPath, child.name)
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return rows.sort((a, b) => (a.order - b.order) || a.id.localeCompare(b.id));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Repository identity (ADR-0016 D1): a catalog entry identifies a
|
|
91
|
+
* REPOSITORY, not a URL string. Strip protocol, .git suffix, trailing
|
|
92
|
+
* slashes and any /tree/<ref>(/...)? browse subtree, then keep the
|
|
93
|
+
* owner/repo tail. Two URLs with the same identity are one rack group. */
|
|
94
|
+
export function repoIdentity(url) {
|
|
95
|
+
const s = String(url || '')
|
|
96
|
+
.replace(/^[a-z+]+:(\/\/)?/i, '') // protocol (https://, git+, github:)
|
|
97
|
+
.split('/tree/')[0] // a github /tree/<ref>/... browse URL is a view INSIDE a repo
|
|
98
|
+
.replace(/\/+$/, '')
|
|
99
|
+
.replace(/\.git$/i, '')
|
|
100
|
+
.replace(/\/+$/, '');
|
|
101
|
+
return s.split('/').slice(-2).join('/');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Shared tail helper: last two path segments. findDepKey keeps a residual
|
|
105
|
+
* subtree (deps may point at subfolders); repoIdentity cuts it entirely. */
|
|
106
|
+
function repoTail(url) {
|
|
107
|
+
const s = String(url || '').replace(/\.git$/i, '').replace(/\/+$/, '');
|
|
108
|
+
return s.split('/').slice(-2).join('/');
|
|
109
|
+
}
|
|
110
|
+
|
|
57
111
|
/** D4 (amended 2026-09-27): the manifest's DEPENDENCIES are the install
|
|
58
112
|
* authority. pnpm records a git plugin under the package's REAL name from
|
|
59
113
|
* its own package.json (e.g. '@temoa/dsh-rules-paths'), not the reff id -
|
|
@@ -64,8 +118,11 @@ export function parseReff(text) {
|
|
|
64
118
|
* install gate. Returns the recorded dep key (the remove target) or null. */
|
|
65
119
|
export function findDepKey(manifest, id, repo) {
|
|
66
120
|
if (manifest.deps[id] !== undefined) return id;
|
|
67
|
-
|
|
68
|
-
|
|
121
|
+
// A github /tree/<ref>/ browse URL is the same repo, not a distinct plugin
|
|
122
|
+
// (identity rule: repoIdentity, ADR-0016 D1); a residual subtree segment is
|
|
123
|
+
// kept here because deps may legitimately point at subfolders.
|
|
124
|
+
const tail = repoTail(String(repo || '').replace(/\/tree\/[^/]+(\/|$)/, '/'));
|
|
125
|
+
if (!tail || tail === '/') return null;
|
|
69
126
|
for (const [key, value] of Object.entries(manifest.deps)) {
|
|
70
127
|
if (typeof value === 'string' && value.includes(tail)) return key;
|
|
71
128
|
}
|
|
@@ -89,39 +146,71 @@ export function isInstalled(manifest, id, repo) {
|
|
|
89
146
|
return findDepKey(manifest, id, repo) !== null;
|
|
90
147
|
}
|
|
91
148
|
|
|
92
|
-
export function buildSnapshot({ reffPath, manifestPath, profile }) {
|
|
149
|
+
export function buildSnapshot({ reffPath, manifestPath, gardenPath, gardenRepo, profile }) {
|
|
93
150
|
const warnings = [];
|
|
94
|
-
if (!existsSync(reffPath)) warnings.push({ code: 'reff-missing', detail: reffPath });
|
|
95
|
-
const parsed = existsSync(reffPath) ? parseReff(readFileSync(reffPath, 'utf8')) : { plugins: [], warnings: [] };
|
|
96
|
-
warnings.push(...parsed.warnings);
|
|
97
151
|
const manifest = readManifest(manifestPath);
|
|
98
152
|
if (manifest.missing) warnings.push({ code: 'manifest-missing', detail: manifestPath });
|
|
153
|
+
const reconcile = (row) => {
|
|
154
|
+
const depKey = findDepKey(manifest, row.id, row.repo);
|
|
155
|
+
return { ...row, installed: depKey !== null, pkg: depKey, bundle: (depKey !== null && manifest.bundles.includes(depKey)) || null };
|
|
156
|
+
};
|
|
157
|
+
// Owned species first (garden scan), then external (reff) - flat rows,
|
|
158
|
+
// renumbered once; sources[] groups them by repo (shelf grammar).
|
|
159
|
+
const owned = scanGarden(gardenPath).map((row) => reconcile({ ...row, repo: gardenRepo ?? row.repo }));
|
|
160
|
+
const parsed = existsSync(reffPath) ? parseReff(readFileSync(reffPath, 'utf8')) : { plugins: [], warnings: [] };
|
|
161
|
+
warnings.push(...parsed.warnings);
|
|
162
|
+
// ADR-0016 D2/D3: an external reff row whose repository the garden already
|
|
163
|
+
// enumerates is SHADOWED — dropped here with a warning; the reff file itself
|
|
164
|
+
// is never rewritten and parseReff output is untouched.
|
|
165
|
+
const ownedIdents = new Set(owned.map((row) => repoIdentity(row.repo)));
|
|
166
|
+
const external = [];
|
|
167
|
+
for (const p of parsed.plugins) {
|
|
168
|
+
if (ownedIdents.has(repoIdentity(p.repo))) {
|
|
169
|
+
warnings.push({ code: 'reff-shadowed-by-garden', detail: p.id });
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
external.push(reconcile({
|
|
173
|
+
n: null,
|
|
174
|
+
id: p.id,
|
|
175
|
+
group: 'external',
|
|
176
|
+
repo: p.repo,
|
|
177
|
+
version: null,
|
|
178
|
+
description: null,
|
|
179
|
+
author: p.author,
|
|
180
|
+
...(p.authorUrl ? { authorUrl: p.authorUrl } : {})
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
const plugins = [...owned, ...external].map((row, i) => ({ ...row, n: String(i + 1) }));
|
|
184
|
+
// sources[]: one collapsible group per REPOSITORY (Shelf Chrome grammar),
|
|
185
|
+
// keyed by repoIdentity (ADR-0016 D2) so /tree browse URLs never split a repo.
|
|
186
|
+
const sources = [];
|
|
187
|
+
const byRepo = new Map();
|
|
188
|
+
for (const row of plugins) {
|
|
189
|
+
const key = repoIdentity(row.repo);
|
|
190
|
+
if (!byRepo.has(key)) {
|
|
191
|
+
sources.push({
|
|
192
|
+
id: key,
|
|
193
|
+
name: key.split('/').pop(),
|
|
194
|
+
author: row.author ?? null,
|
|
195
|
+
repo: row.repo,
|
|
196
|
+
plugins: []
|
|
197
|
+
});
|
|
198
|
+
byRepo.set(key, sources[sources.length - 1]);
|
|
199
|
+
}
|
|
200
|
+
byRepo.get(key).plugins.push(row);
|
|
201
|
+
}
|
|
99
202
|
return {
|
|
100
203
|
snapshot: {
|
|
101
204
|
v: CACHE_VERSION,
|
|
102
205
|
generatedAt: new Date().toISOString(),
|
|
103
206
|
profile,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
n: String(i + 1),
|
|
108
|
-
id: p.id,
|
|
109
|
-
repo: p.repo,
|
|
110
|
-
author: p.author,
|
|
111
|
-
...(p.authorUrl ? { authorUrl: p.authorUrl } : {}),
|
|
112
|
-
installed: depKey !== null,
|
|
113
|
-
// The recorded dependency name (pnpm's truth) - the remove target.
|
|
114
|
-
pkg: depKey,
|
|
115
|
-
bundle: (depKey !== null && manifest.bundles.includes(depKey)) || null
|
|
116
|
-
};
|
|
117
|
-
})
|
|
207
|
+
sources,
|
|
208
|
+
plugins
|
|
118
209
|
},
|
|
119
210
|
warnings
|
|
120
211
|
};
|
|
121
212
|
}
|
|
122
213
|
|
|
123
|
-
/** D3: the reff URL becomes the dsh add spec: git+<url>.git (append .git
|
|
124
|
-
* only when absent — the operator's line stays the single source). */
|
|
125
214
|
export function toInstallSpec(repo) {
|
|
126
215
|
return 'git+' + repo + (repo.endsWith('.git') ? '' : '.git');
|
|
127
216
|
}
|
|
@@ -132,6 +221,47 @@ function resolveDshCommand(flags) {
|
|
|
132
221
|
return 'npx --yes @deepseek-ai/dsh@latest';
|
|
133
222
|
}
|
|
134
223
|
|
|
224
|
+
|
|
225
|
+
/** Owned-species install (Plugin Garden ADR, D3): write the link dependency,
|
|
226
|
+
* APPEND (never rewrite) the bundle entry, idempotent on re-run. The only
|
|
227
|
+
* manifest write the rack performs itself - scoped to @local link species;
|
|
228
|
+
* git/npm species keep the dsh add delegation (D3). Returns the dep key. */
|
|
229
|
+
export function installOwned(manifestPath, row) {
|
|
230
|
+
const doc = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
231
|
+
doc.dependencies = doc.dependencies ?? {};
|
|
232
|
+
doc.dependencies[row.id] = row.installSpec;
|
|
233
|
+
const bundles = doc.dsh && doc.dsh.profile && Array.isArray(doc.dsh.profile.bundles) ? doc.dsh.profile.bundles : [];
|
|
234
|
+
if (!bundles.includes(row.id)) bundles.push(row.id);
|
|
235
|
+
if (doc.dsh && doc.dsh.profile) doc.dsh.profile.bundles = bundles;
|
|
236
|
+
writeFileSync(manifestPath, JSON.stringify(doc, null, 2));
|
|
237
|
+
return row.id;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Clobber guard (Plugin Garden ADR, 2026-10-07): a dsh plugin add rewrites
|
|
241
|
+
* the bundles array to just the new plugin and may drop earlier deps.
|
|
242
|
+
* Re-append every still-installed owned row's bundle entry and dep entry.
|
|
243
|
+
* Rows already recorded pass through untouched. */
|
|
244
|
+
export function restoreClobberedBundles(manifestPath, preOwnedRows) {
|
|
245
|
+
const doc = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
246
|
+
let changed = false;
|
|
247
|
+
doc.dependencies = doc.dependencies ?? {};
|
|
248
|
+
const bundles = doc.dsh && doc.dsh.profile && Array.isArray(doc.dsh.profile.bundles) ? doc.dsh.profile.bundles : [];
|
|
249
|
+
for (const row of preOwnedRows) {
|
|
250
|
+
if (!row.pkg && !row.installSpec) continue;
|
|
251
|
+
if (doc.dependencies[row.id] === undefined) {
|
|
252
|
+
doc.dependencies[row.id] = row.installSpec;
|
|
253
|
+
changed = true;
|
|
254
|
+
}
|
|
255
|
+
if (!bundles.includes(row.id)) {
|
|
256
|
+
bundles.push(row.id);
|
|
257
|
+
changed = true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (doc.dsh && doc.dsh.profile) doc.dsh.profile.bundles = bundles;
|
|
261
|
+
if (changed) writeFileSync(manifestPath, JSON.stringify(doc, null, 2));
|
|
262
|
+
return changed;
|
|
263
|
+
}
|
|
264
|
+
|
|
135
265
|
/** D3/D6: run dsh, relay its own diagnostics verbatim on failure. */
|
|
136
266
|
export function runDsh(flags, action, spec) {
|
|
137
267
|
const cmd = resolveDshCommand(flags) + ' plugin --profile ' + flags.profile + ' ' + action + ' ' + spec;
|
|
@@ -166,7 +296,7 @@ function main() {
|
|
|
166
296
|
console.log(JSON.stringify({ v: CACHE_VERSION, ok: true, reused: true, snapshot: loadSnapshot(cachePath) }));
|
|
167
297
|
return;
|
|
168
298
|
}
|
|
169
|
-
const { snapshot, warnings } = buildSnapshot({ reffPath, manifestPath, profile: args.profile });
|
|
299
|
+
const { snapshot, warnings } = buildSnapshot({ reffPath, manifestPath, gardenPath: args.garden, gardenRepo: args.gardenRepo, profile: args.profile });
|
|
170
300
|
mkdirSync(dirname(cachePath), { recursive: true });
|
|
171
301
|
writeFileSync(cachePath, JSON.stringify(snapshot, null, 2));
|
|
172
302
|
console.log(JSON.stringify({ v: CACHE_VERSION, ok: true, snapshot, ...(warnings.length ? { errors: warnings.map((w) => w.code + ': ' + w.detail) } : {}) }));
|
|
@@ -186,6 +316,17 @@ function main() {
|
|
|
186
316
|
if (!plugin) { results.push({ n: t, ok: false, error: 'not on the rack: ' + t }); continue; }
|
|
187
317
|
if (action === 'install') {
|
|
188
318
|
if (plugin.installed) { results.push({ n: plugin.n, id: plugin.id, ok: true, already: true }); continue; }
|
|
319
|
+
if (plugin.group === 'owned') {
|
|
320
|
+
// Owned species (Plugin Garden ADR, D3-scoped): link dep + bundles append + pnpm install.
|
|
321
|
+
const res = installOwned(manifestPath, plugin);
|
|
322
|
+
const inst = spawnSync('pnpm', ['install'], { cwd: dirname(manifestPath), encoding: 'utf8', timeout: 300000 });
|
|
323
|
+
if (inst.status !== 0) { results.push({ n: plugin.n, id: plugin.id, ok: false, error: 'pnpm install failed (exit ' + (inst.status ?? 1) + '): ' + String(inst.stderr ?? '').slice(-400) }); continue; }
|
|
324
|
+
plugin.installed = true;
|
|
325
|
+
plugin.pkg = res;
|
|
326
|
+
plugin.bundle = true;
|
|
327
|
+
results.push({ n: plugin.n, id: plugin.id, pkg: res, ok: true });
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
189
330
|
const r = runDsh(args, 'add', toInstallSpec(plugin.repo));
|
|
190
331
|
if (!r.ok) { results.push({ n: plugin.n, id: plugin.id, ok: false, error: r.error }); continue; }
|
|
191
332
|
const manifest = readManifest(manifestPath);
|
|
@@ -234,4 +375,5 @@ function fail(errors) {
|
|
|
234
375
|
process.exit(1);
|
|
235
376
|
}
|
|
236
377
|
|
|
237
|
-
|
|
378
|
+
// Run only when executed directly (not when imported by tests/other tools).
|
|
379
|
+
if (process.argv[1] && import.meta.url === new URL('file://' + process.argv[1].replace(/\\/g, '/')).href) main();
|