@aiwayds/dsh-dcp 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +2 -0
- package/README.md +2 -0
- package/lib/config.js +3 -3
- package/lib/index.js +10 -0
- package/lib/setup.js +1 -1
- package/lib/summarizer.js +4 -4
- package/package.json +54 -35
- package/scripts/compare.mjs +1 -1
- package/scripts/link-dsh-closure.mjs +100 -0
- package/scripts/smoke-boot.mjs +122 -0
package/README.en.md
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
Deterministic context-compaction backend for dsh (DeepSeek Harness): **context
|
|
4
4
|
compaction without an LLM call**, works out of the box.
|
|
5
5
|
|
|
6
|
+
**Requires dsh >= 0.1.2-rc.1** — this plugin targets the dsh RC/stable line only (CI and releases resolve the newest of the `latest`/`next` dist-tags at runtime). **The alpha line is no longer supported.**
|
|
7
|
+
|
|
6
8
|
> [简体中文](README.md) · **English**
|
|
7
9
|
|
|
8
10
|
## Why
|
package/README.md
CHANGED
package/lib/config.js
CHANGED
|
@@ -60,11 +60,11 @@ export function splitConfig(config = {}) {
|
|
|
60
60
|
for (const key of Object.keys(config)) {
|
|
61
61
|
if (!known.has(key)) throw new Error(`DcpConfig: unknown key "${key}"`)
|
|
62
62
|
}
|
|
63
|
-
const basic = {}
|
|
63
|
+
const basic = /** @type {Record<string, unknown>} */ ({})
|
|
64
64
|
for (const key of BASIC_CONFIG_KEYS) {
|
|
65
65
|
if (config[key] !== undefined) basic[key] = config[key]
|
|
66
66
|
}
|
|
67
|
-
const dcp = {}
|
|
67
|
+
const dcp = /** @type {Record<string, unknown>} */ ({})
|
|
68
68
|
for (const key of DCP_CONFIG_KEYS) {
|
|
69
69
|
if (config[key] !== undefined) dcp[key] = config[key]
|
|
70
70
|
}
|
|
@@ -75,7 +75,7 @@ export function splitConfig(config = {}) {
|
|
|
75
75
|
* Validate and resolve dcp defaults.
|
|
76
76
|
*
|
|
77
77
|
* @param {Record<string, unknown>} raw - the dcp half of {@link splitConfig}.
|
|
78
|
-
* @returns {Readonly<{dedup: boolean, purgeErrors: boolean, maxItems: number, maxItemChars: number, maxSummaryTokens: number, language: 'en'|'zh', tokenEstimate: 'cjk'|'ascii', protectedTools: readonly string[]}>}
|
|
78
|
+
* @returns {Readonly<{dedup: boolean, purgeErrors: boolean, maxItems: number, maxItemChars: number, maxSummaryTokens: number, language: 'en'|'zh', tokenEstimate: 'cjk'|'ascii', protectedTools: readonly string[], roundInterval: number, notice: boolean}>}
|
|
79
79
|
*/
|
|
80
80
|
export function resolveDcpConfig(raw = {}) {
|
|
81
81
|
if (raw.dedup !== undefined && typeof raw.dedup !== 'boolean') {
|
package/lib/index.js
CHANGED
|
@@ -50,6 +50,11 @@ const { version: VERSION } = require('../package.json')
|
|
|
50
50
|
* "Cannot read private member #triggerLabels from an object whose class did
|
|
51
51
|
* not declare it", while symbol lookups traverse those receivers to this
|
|
52
52
|
* instance's own state.
|
|
53
|
+
*
|
|
54
|
+
* Verified against dsh 0.1.2-alpha.3 (cordis 4.0.2): `withProps` /
|
|
55
|
+
* `createTraceable` / `applyTraceable` receivers and the `ctx.mixin` service
|
|
56
|
+
* forwarding are unchanged, and the base engine still brands no `#private`
|
|
57
|
+
* state — the symbol-keyed approach remains both necessary and valid.
|
|
53
58
|
*/
|
|
54
59
|
const kRounds = Symbol('dsh-dcp.rounds')
|
|
55
60
|
const kTriggerLabels = Symbol('dsh-dcp.triggerLabels')
|
|
@@ -111,6 +116,7 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
111
116
|
dcp
|
|
112
117
|
|
|
113
118
|
/** Compaction counters surfaced by `/dcp`. */
|
|
119
|
+
/** @type {{compactions: number, shadowedTokens: number, lastAt: number|null}} */
|
|
114
120
|
dcpStats
|
|
115
121
|
|
|
116
122
|
/** Absolute module path, echoed by `/dcp set` for persistence snippets. */
|
|
@@ -230,6 +236,10 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
230
236
|
* round-interval trigger — the parent's `compactNow` bypasses
|
|
231
237
|
* `compactRegion` (it drives `compactSurfaceRegion` directly), so without
|
|
232
238
|
* this override the manual path would miss stats and the transcript notice.
|
|
239
|
+
*
|
|
240
|
+
* alpha.3 note: the base dereferences `signal` unguarded
|
|
241
|
+
* (`signal.throwIfAborted()`), so the signal is required — both this
|
|
242
|
+
* plugin's round trigger and the command invocation always pass one.
|
|
233
243
|
*/
|
|
234
244
|
async compactNow(agent, signal, sourceCommandId) {
|
|
235
245
|
const session = agent.session
|
package/lib/setup.js
CHANGED
|
@@ -68,7 +68,7 @@ export function findBundledProfiles(profilesDir, pkg) {
|
|
|
68
68
|
*
|
|
69
69
|
* @param {string|undefined} text - existing file content, or undefined when the file is absent.
|
|
70
70
|
* @param {{ name: string }} options - the resolvable dsh-dcp entry specifier to mount.
|
|
71
|
-
* @returns {{ action: 'create'|'patch'
|
|
71
|
+
* @returns {{ action: 'create'|'patch', block: string, note?: string } | { action: 'skip' }}
|
|
72
72
|
*/
|
|
73
73
|
export function planPatch(text, { name }) {
|
|
74
74
|
if (text === undefined || text === null) {
|
package/lib/summarizer.js
CHANGED
|
@@ -105,7 +105,7 @@ export function estimateTextTokens(text, mode = 'cjk') {
|
|
|
105
105
|
* tool-result payloads (the same content the host meter prices, minus its
|
|
106
106
|
* per-block overhead).
|
|
107
107
|
*
|
|
108
|
-
* @param {{ content?: Array<
|
|
108
|
+
* @param {{ content?: Array<Record<string, any>> }} message - any message-shaped object.
|
|
109
109
|
* @param {'cjk'|'ascii'} [mode] - pricing mode, see {@link estimateTextTokens}.
|
|
110
110
|
* @returns {number} estimated tokens.
|
|
111
111
|
*/
|
|
@@ -275,7 +275,7 @@ function argDisplay(name, parsed) {
|
|
|
275
275
|
* @returns {object} the extracted fact bundle.
|
|
276
276
|
*/
|
|
277
277
|
export function extractFacts(messages, language = 'en') {
|
|
278
|
-
const facts = {
|
|
278
|
+
const facts = /** @type {{messageCount: number, intents: string[], files: Map<string, {reads: number, writes: number}>, commands: string[], errors: string[], pendingTodos: string[], concepts: Set<string>, dupCounts: Map<string, {name: string, parsed: any, count: number}>, toolCallCount: number, lastUserText: string, lastAssistantText: string, carried: Map<string, string[]>}} */ ({
|
|
279
279
|
messageCount: messages.length,
|
|
280
280
|
intents: [],
|
|
281
281
|
files: new Map(),
|
|
@@ -288,7 +288,7 @@ export function extractFacts(messages, language = 'en') {
|
|
|
288
288
|
lastUserText: '',
|
|
289
289
|
lastAssistantText: '',
|
|
290
290
|
carried: new Map(),
|
|
291
|
-
}
|
|
291
|
+
})
|
|
292
292
|
const callsById = new Map()
|
|
293
293
|
const errorRegexes = errorPatterns(language)
|
|
294
294
|
const { checkbox, markers } = todoPatterns(language)
|
|
@@ -521,7 +521,7 @@ function composeTerse(facts, t, itemChars) {
|
|
|
521
521
|
*
|
|
522
522
|
* @param {{ messages: import('@deepseek-ai/dsh-llm').Message[] }} input - replayed region.
|
|
523
523
|
* @param {object} dcp - resolved dcp config.
|
|
524
|
-
* @param {(message:
|
|
524
|
+
* @param {(message: any) => number} [estimateMessageLike] - token estimator; defaults to {@link estimateMessageTokens}.
|
|
525
525
|
* @returns {{ summary: { type: 'text', text: string }[], provider: string, model: string }}
|
|
526
526
|
*/
|
|
527
527
|
export function summarizeDeterministically(input, dcp, estimateMessageLike = (message) => estimateMessageTokens(message, dcp.tokenEstimate)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiwayds/dsh-dcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Deterministic context-pruning compaction backend for dsh (DeepSeek Harness) — zero-LLM summaries, /dcp command, works out of the box. Design references Opencode-DCP/opencode-dynamic-context-pruning.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
26
|
"test": "node --test",
|
|
27
|
-
"setup": "node scripts/setup.mjs"
|
|
27
|
+
"setup": "node scripts/setup.mjs",
|
|
28
|
+
"link:closure": "node scripts/link-dsh-closure.mjs",
|
|
29
|
+
"check": "tsc --noEmit -p tsconfig.json",
|
|
30
|
+
"smoke": "node scripts/smoke-boot.mjs"
|
|
28
31
|
},
|
|
29
32
|
"keywords": [
|
|
30
33
|
"dsh",
|
|
@@ -48,18 +51,18 @@
|
|
|
48
51
|
},
|
|
49
52
|
"dependencies": {},
|
|
50
53
|
"peerDependencies": {
|
|
51
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
52
|
-
"@deepseek-ai/dsh-agent": "0.1.
|
|
53
|
-
"@deepseek-ai/dsh-brand": "0.1.
|
|
54
|
-
"@deepseek-ai/dsh-commands": "0.1.
|
|
55
|
-
"@deepseek-ai/dsh-compaction": "0.1.
|
|
56
|
-
"@deepseek-ai/dsh-compaction-basic": "0.1.
|
|
57
|
-
"@deepseek-ai/dsh-compaction-tool-result-pruner": "0.1.
|
|
58
|
-
"@deepseek-ai/dsh-invariants": "0.1.
|
|
59
|
-
"@deepseek-ai/dsh-llm": "0.1.
|
|
60
|
-
"@deepseek-ai/dsh-session": "0.1.
|
|
61
|
-
"@deepseek-ai/dsh-token-meter": "0.1.
|
|
62
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
54
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
55
|
+
"@deepseek-ai/dsh-agent": ">=0.1.2-rc.1",
|
|
56
|
+
"@deepseek-ai/dsh-brand": ">=0.1.2-rc.1",
|
|
57
|
+
"@deepseek-ai/dsh-commands": ">=0.1.2-rc.1",
|
|
58
|
+
"@deepseek-ai/dsh-compaction": ">=0.1.2-rc.1",
|
|
59
|
+
"@deepseek-ai/dsh-compaction-basic": ">=0.1.2-rc.1",
|
|
60
|
+
"@deepseek-ai/dsh-compaction-tool-result-pruner": ">=0.1.2-rc.1",
|
|
61
|
+
"@deepseek-ai/dsh-invariants": ">=0.1.2-rc.1",
|
|
62
|
+
"@deepseek-ai/dsh-llm": ">=0.1.2-rc.1",
|
|
63
|
+
"@deepseek-ai/dsh-session": ">=0.1.2-rc.1",
|
|
64
|
+
"@deepseek-ai/dsh-token-meter": ">=0.1.2-rc.1",
|
|
65
|
+
"@deepseek-ai/schemastery": "^3.18.2"
|
|
63
66
|
},
|
|
64
67
|
"peerDependenciesMeta": {
|
|
65
68
|
"@deepseek-ai/cordis": {
|
|
@@ -100,33 +103,49 @@
|
|
|
100
103
|
}
|
|
101
104
|
},
|
|
102
105
|
"devDependencies": {
|
|
103
|
-
"@
|
|
104
|
-
"@deepseek-ai/
|
|
105
|
-
"@deepseek-ai/dsh-
|
|
106
|
-
"@deepseek-ai/dsh-
|
|
107
|
-
"@deepseek-ai/dsh-
|
|
108
|
-
"@deepseek-ai/dsh-compaction
|
|
109
|
-
"@deepseek-ai/dsh-compaction-
|
|
110
|
-
"@deepseek-ai/dsh-
|
|
111
|
-
"@deepseek-ai/dsh-
|
|
112
|
-
"@deepseek-ai/dsh-
|
|
113
|
-
"@deepseek-ai/dsh-
|
|
114
|
-
"@deepseek-ai/
|
|
106
|
+
"@types/node": "^24.0.0",
|
|
107
|
+
"@deepseek-ai/cordis": "4.0.2",
|
|
108
|
+
"@deepseek-ai/dsh-agent": "0.1.2-rc.1",
|
|
109
|
+
"@deepseek-ai/dsh-brand": "0.1.2-rc.1",
|
|
110
|
+
"@deepseek-ai/dsh-commands": "0.1.2-rc.1",
|
|
111
|
+
"@deepseek-ai/dsh-compaction": "0.1.2-rc.1",
|
|
112
|
+
"@deepseek-ai/dsh-compaction-basic": "0.1.2-rc.1",
|
|
113
|
+
"@deepseek-ai/dsh-compaction-tool-result-pruner": "0.1.2-rc.1",
|
|
114
|
+
"@deepseek-ai/dsh-invariants": "0.1.2-rc.1",
|
|
115
|
+
"@deepseek-ai/dsh-llm": "0.1.2-rc.1",
|
|
116
|
+
"@deepseek-ai/dsh-session": "0.1.2-rc.1",
|
|
117
|
+
"@deepseek-ai/dsh-token-meter": "0.1.2-rc.1",
|
|
118
|
+
"@deepseek-ai/schemastery": "3.18.2",
|
|
119
|
+
"typescript": "~5.9.0"
|
|
115
120
|
},
|
|
116
121
|
"overrides": {
|
|
117
|
-
"@deepseek-ai/dsh-agent": "0.1.
|
|
118
|
-
"@deepseek-ai/dsh-brand": "0.1.
|
|
119
|
-
"@deepseek-ai/dsh-commands": "0.1.
|
|
120
|
-
"@deepseek-ai/dsh-compaction": "0.1.
|
|
121
|
-
"@deepseek-ai/dsh-compaction-tool-result-pruner": "0.1.
|
|
122
|
-
"@deepseek-ai/dsh-invariants": "0.1.
|
|
123
|
-
"@deepseek-ai/dsh-llm": "0.1.
|
|
124
|
-
"@deepseek-ai/dsh-session": "0.1.
|
|
125
|
-
"@deepseek-ai/dsh-token-meter": "0.1.
|
|
122
|
+
"@deepseek-ai/dsh-agent": "0.1.2-rc.1",
|
|
123
|
+
"@deepseek-ai/dsh-brand": "0.1.2-rc.1",
|
|
124
|
+
"@deepseek-ai/dsh-commands": "0.1.2-rc.1",
|
|
125
|
+
"@deepseek-ai/dsh-compaction": "0.1.2-rc.1",
|
|
126
|
+
"@deepseek-ai/dsh-compaction-tool-result-pruner": "0.1.2-rc.1",
|
|
127
|
+
"@deepseek-ai/dsh-invariants": "0.1.2-rc.1",
|
|
128
|
+
"@deepseek-ai/dsh-llm": "0.1.2-rc.1",
|
|
129
|
+
"@deepseek-ai/dsh-session": "0.1.2-rc.1",
|
|
130
|
+
"@deepseek-ai/dsh-token-meter": "0.1.2-rc.1",
|
|
131
|
+
"@deepseek-ai/dsh-scope": "0.1.2-rc.1",
|
|
132
|
+
"@deepseek-ai/dsh-attachment": "0.1.2-rc.1",
|
|
133
|
+
"@deepseek-ai/dsh-session-projection": "0.1.2-rc.1",
|
|
134
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.2-rc.1",
|
|
135
|
+
"@deepseek-ai/dsh-llm-retry": "0.1.2-rc.1"
|
|
126
136
|
},
|
|
127
137
|
"dsh": {
|
|
128
138
|
"bundle": {
|
|
129
139
|
"patch": "./cordis.patch.yml"
|
|
130
140
|
}
|
|
141
|
+
},
|
|
142
|
+
"pnpm": {
|
|
143
|
+
"overrides": {
|
|
144
|
+
"@deepseek-ai/dsh-scope": "0.1.2-rc.1",
|
|
145
|
+
"@deepseek-ai/dsh-attachment": "0.1.2-rc.1",
|
|
146
|
+
"@deepseek-ai/dsh-session-projection": "0.1.2-rc.1",
|
|
147
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.2-rc.1",
|
|
148
|
+
"@deepseek-ai/dsh-llm-retry": "0.1.2-rc.1"
|
|
149
|
+
}
|
|
131
150
|
}
|
|
132
151
|
}
|
package/scripts/compare.mjs
CHANGED
|
@@ -175,7 +175,7 @@ function fmt(n) {
|
|
|
175
175
|
*/
|
|
176
176
|
function simulate(events, mode, useDcp) {
|
|
177
177
|
const surface = []
|
|
178
|
-
const stats = { requests: 0, inputTokens: 0, hitTokens: 0, compactions: [] }
|
|
178
|
+
const stats = /** @type {{ requests: number, inputTokens: number, hitTokens: number, compactions: Array<{regionNodes: number, regionHost: number, regionCjk: number, summaryHost: number, summaryCjk: number, compression: string, totalAfter: number, preview: string}> }} */ ({ requests: 0, inputTokens: 0, hitTokens: 0, compactions: [] })
|
|
179
179
|
let prevSurface = []
|
|
180
180
|
let compactionId = 0
|
|
181
181
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Closure linker: point every `node_modules/@deepseek-ai/*` entry at one dsh
|
|
3
|
+
* closure (the installed dsh CLI's own `node_modules/@deepseek-ai` tree).
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: dsh-dcp is a plugin that runs *inside* the installed dsh
|
|
6
|
+
* CLI, and its source imports the `@deepseek-ai/*` host packages (cordis,
|
|
7
|
+
* dsh-compaction-basic, dsh-llm, …). This repo is lib-as-source — there is no
|
|
8
|
+
* `src/` and no build step — so typecheck (`pnpm check`, tsc checkJs) and the
|
|
9
|
+
* node:test suite must resolve those imports against exactly ONE copy of the
|
|
10
|
+
* host graph: the closure the real runtime uses. Linking also guarantees a
|
|
11
|
+
* single `@deepseek-ai/cordis` instance, which the `declare module`
|
|
12
|
+
* augmentations (e.g. `ctx.commands`) require.
|
|
13
|
+
*
|
|
14
|
+
* NOTE on the guard below: unlike a TS repo this repo has no `src/` directory
|
|
15
|
+
* — `lib/` IS the source — so the "am I inside the repo?" check looks for
|
|
16
|
+
* `lib/` (plus `.git/`, which a published tarball never carries). The script
|
|
17
|
+
* is never wired as a lifecycle hook (no postinstall); it is run explicitly
|
|
18
|
+
* (CI step, `pnpm link:closure`), and the guard only protects stray manual
|
|
19
|
+
* runs from outside the repo.
|
|
20
|
+
*
|
|
21
|
+
* Resolution order for the closure:
|
|
22
|
+
* 0) `$DSH_CLOSURE_DIR` — explicit override, e.g. a scratch closure from
|
|
23
|
+
* `npm i --prefix ~/tmp/dsh-alpha-closure @deepseek-ai/dsh@alpha`:
|
|
24
|
+
* DSH_CLOSURE_DIR=~/tmp/dsh-alpha-closure/node_modules/@deepseek-ai node scripts/link-dsh-closure.mjs
|
|
25
|
+
* 1) the `dsh` bin on PATH → its package's node_modules/@deepseek-ai
|
|
26
|
+
* 2) `npm root -g` nested closure, then npm's flat global @deepseek-ai scope
|
|
27
|
+
*
|
|
28
|
+
* It is a no-op (exit 0) when no dsh closure is found.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { execFileSync } from 'node:child_process'
|
|
32
|
+
import { existsSync, mkdirSync, readdirSync, realpathSync, rmSync, symlinkSync } from 'node:fs'
|
|
33
|
+
import { dirname, join } from 'node:path'
|
|
34
|
+
import { fileURLToPath } from 'node:url'
|
|
35
|
+
|
|
36
|
+
const repoRoot = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
37
|
+
// Guard: dev convenience for THIS repo only. lib/ is this repo's source (there
|
|
38
|
+
// is no src/), and .git/ only exists in a working clone — never in a packed
|
|
39
|
+
// tarball — so both together prove "inside the repo, not a consumer install".
|
|
40
|
+
if (!existsSync(join(repoRoot, 'lib')) || !existsSync(join(repoRoot, '.git'))) {
|
|
41
|
+
process.exit(0)
|
|
42
|
+
}
|
|
43
|
+
const scopeDir = join(repoRoot, 'node_modules', '@deepseek-ai')
|
|
44
|
+
|
|
45
|
+
/** The dsh closure: a node_modules dir whose @deepseek-ai scope is complete. */
|
|
46
|
+
function findDshClosure() {
|
|
47
|
+
// 0) Explicit override for dev/typecheck against an unreleased dsh line.
|
|
48
|
+
const override = process.env.DSH_CLOSURE_DIR
|
|
49
|
+
if (override !== undefined && override !== '') {
|
|
50
|
+
const dir = realpathSync(override)
|
|
51
|
+
if (existsSync(join(dir, 'cordis'))) return dir
|
|
52
|
+
console.warn(`[link-dsh-closure] DSH_CLOSURE_DIR=${override} lacks @deepseek-ai/cordis — ignoring override`)
|
|
53
|
+
}
|
|
54
|
+
// 1) Follow the `dsh` bin — the most faithful pointer to the installed CLI
|
|
55
|
+
// (`/opt/homebrew/bin/dsh` → …/lib/bin.js → pkg dir → its node_modules).
|
|
56
|
+
try {
|
|
57
|
+
const bin = execFileSync('which', ['dsh'], { encoding: 'utf8' }).trim()
|
|
58
|
+
if (bin !== '') {
|
|
59
|
+
const real = realpathSync(bin)
|
|
60
|
+
const closure = join(dirname(dirname(real)), 'node_modules', '@deepseek-ai')
|
|
61
|
+
if (existsSync(join(closure, 'cordis'))) return closure
|
|
62
|
+
}
|
|
63
|
+
} catch { /* dsh not on PATH */ }
|
|
64
|
+
// 2) Fall back to the global node_modules root — the dsh package's own
|
|
65
|
+
// nested closure (what current npm produces for a global install).
|
|
66
|
+
try {
|
|
67
|
+
const root = execFileSync('npm', ['root', '-g'], { encoding: 'utf8' }).trim()
|
|
68
|
+
const nested = join(root, '@deepseek-ai', 'dsh', 'node_modules', '@deepseek-ai')
|
|
69
|
+
if (existsSync(join(nested, 'cordis'))) return nested
|
|
70
|
+
// 3) Last resort: npm's flat global layout, where dsh's @deepseek-ai/*
|
|
71
|
+
// deps are hoisted straight into <npm root -g>/@deepseek-ai next to
|
|
72
|
+
// the dsh package itself.
|
|
73
|
+
const flat = join(root, '@deepseek-ai')
|
|
74
|
+
if (existsSync(join(flat, 'cordis'))) return flat
|
|
75
|
+
} catch { /* npm unavailable */ }
|
|
76
|
+
return undefined
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const closure = findDshClosure()
|
|
80
|
+
if (closure === undefined) {
|
|
81
|
+
console.warn('[link-dsh-closure] no dsh closure found — skipping @deepseek-ai links (dev without dsh)')
|
|
82
|
+
process.exit(0)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
mkdirSync(scopeDir, { recursive: true })
|
|
86
|
+
let linked = 0
|
|
87
|
+
for (const name of readdirSync(closure)) {
|
|
88
|
+
const target = join(scopeDir, name)
|
|
89
|
+
const source = join(closure, name)
|
|
90
|
+
try {
|
|
91
|
+
// Replace any existing entry (stale symlink, or a local .pnpm copy a
|
|
92
|
+
// previous install created) with the closure link.
|
|
93
|
+
rmSync(target, { recursive: true, force: true })
|
|
94
|
+
symlinkSync(source, target, 'junction')
|
|
95
|
+
linked++
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.warn(`[link-dsh-closure] failed to link ${name}: ${(error instanceof Error ? error.message : String(error))}`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
console.log(`[link-dsh-closure] linked ${linked} @deepseek-ai/* packages from ${closure}`)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Boot-smoke: mount the freshly packed dsh-dcp into a scratch dsh profile and
|
|
3
|
+
// boot it with the real dsh CLI.
|
|
4
|
+
//
|
|
5
|
+
// Unlike dsh-cron (which mounts through the bundle patch), this exercises the
|
|
6
|
+
// cordis.patch.yml mechanism — the same layer scripts/setup.mjs writes for
|
|
7
|
+
// users: the profile patch disables the default `compaction-basic` backend and
|
|
8
|
+
// inserts dsh-dcp by absolute path.
|
|
9
|
+
//
|
|
10
|
+
// 1. npm pack the repo → tarball
|
|
11
|
+
// 2. scratch $DSH_HOME/profiles/smoke: the dsh-base bundle plus the tarball
|
|
12
|
+
// as a file: dependency, and a cordis.patch.yml carrying the
|
|
13
|
+
// disable-compaction-basic + insert-dsh-dcp mount block
|
|
14
|
+
// 3. pnpm install in the profile
|
|
15
|
+
// 4. `dsh --profile smoke --dump-config` must compose dsh-dcp into the tree
|
|
16
|
+
// (mount/patch-layer proof)
|
|
17
|
+
// 5. a real boot under a timeout must load the plugin tree without a loader
|
|
18
|
+
// error (a healthy boot is silent and survives to the kill signal; a
|
|
19
|
+
// broken plugin dies within ~1s with the loader error)
|
|
20
|
+
//
|
|
21
|
+
// The dsh CLI comes from $DSH_BIN if set (e.g. a scratch alpha closure:
|
|
22
|
+
// DSH_BIN=~/tmp/dsh-alpha-closure/node_modules/@deepseek-ai/dsh/lib/bin.js),
|
|
23
|
+
// otherwise `dsh` on PATH.
|
|
24
|
+
//
|
|
25
|
+
// Exit 0 = mounted and boots clean. Temp dir is kept and printed on failure,
|
|
26
|
+
// removed on success.
|
|
27
|
+
|
|
28
|
+
import { spawnSync } from 'node:child_process'
|
|
29
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
30
|
+
import { readFile } from 'node:fs/promises'
|
|
31
|
+
import { tmpdir } from 'node:os'
|
|
32
|
+
import path from 'node:path'
|
|
33
|
+
import { fileURLToPath } from 'node:url'
|
|
34
|
+
|
|
35
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
36
|
+
const pkg = JSON.parse(await readFile(path.join(repoRoot, 'package.json'), 'utf8'))
|
|
37
|
+
const ownName = pkg.name // @aiwayds/dsh-dcp
|
|
38
|
+
const entryId = 'dsh-dcp'
|
|
39
|
+
|
|
40
|
+
const work = mkdtempSync(path.join(tmpdir(), 'dsh-dcp-smoke-'))
|
|
41
|
+
const home = path.join(work, 'dsh-home')
|
|
42
|
+
const profile = path.join(home, 'profiles', 'smoke')
|
|
43
|
+
mkdirSync(profile, { recursive: true })
|
|
44
|
+
|
|
45
|
+
function fail(message, output = '') {
|
|
46
|
+
console.error(`smoke-boot: FAIL — ${message}`)
|
|
47
|
+
if (output) console.error(output.split('\n').slice(0, 30).join('\n'))
|
|
48
|
+
console.error(`smoke-boot: scratch kept at ${work}`)
|
|
49
|
+
process.exit(1)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const dshBin = process.env.DSH_BIN || 'dsh'
|
|
53
|
+
const dsh = (args, opts = {}) => spawnSync(dshBin, args, { cwd: profile, encoding: 'utf8', env: { ...process.env, DSH_HOME: home }, ...opts })
|
|
54
|
+
|
|
55
|
+
const pack = spawnSync('npm', ['pack', '--pack-destination', work], { cwd: repoRoot, encoding: 'utf8' })
|
|
56
|
+
if (pack.status !== 0 || pack.error) fail('npm pack failed', `${pack.stdout}\n${pack.stderr}`)
|
|
57
|
+
const tarball = path.join(work, pack.stdout.trim().split('\n').at(-1) ?? '')
|
|
58
|
+
|
|
59
|
+
writeFileSync(path.join(profile, 'cordis.yml'), '# dsh profile root — empty; the tree is composed from the bundle patches\n[]\n')
|
|
60
|
+
// The mount layer under test — isomorphic with scripts/setup.mjs's output:
|
|
61
|
+
// compaction-basic disabled, dsh-dcp inserted by absolute entry path.
|
|
62
|
+
writeFileSync(path.join(profile, 'cordis.patch.yml'), `# scratch smoke profile: dsh-dcp mounted the setup.mjs way
|
|
63
|
+
- id: compaction-basic
|
|
64
|
+
disabled: true
|
|
65
|
+
- insert:
|
|
66
|
+
- id: ${entryId}
|
|
67
|
+
name: ${path.join(profile, 'node_modules', '@aiwayds', 'dsh-dcp', 'lib', 'index.js')}
|
|
68
|
+
config:
|
|
69
|
+
thresholdRatio: 0.7
|
|
70
|
+
language: zh
|
|
71
|
+
`)
|
|
72
|
+
writeFileSync(path.join(profile, 'pnpm-workspace.yaml'), 'packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n')
|
|
73
|
+
writeFileSync(path.join(profile, 'package.json'), JSON.stringify({
|
|
74
|
+
name: 'dsh-profile-smoke',
|
|
75
|
+
private: true,
|
|
76
|
+
dependencies: {
|
|
77
|
+
[ownName]: `file:${tarball}`,
|
|
78
|
+
},
|
|
79
|
+
dsh: {
|
|
80
|
+
profile: {
|
|
81
|
+
// dsh-base brings the standard tree (including compaction-basic) that
|
|
82
|
+
// the patch layer then re-wires; dsh-dcp itself mounts via the patch.
|
|
83
|
+
bundles: [
|
|
84
|
+
'@deepseek-ai/dsh-base',
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
}, null, 2) + '\n')
|
|
89
|
+
|
|
90
|
+
const install = spawnSync('pnpm', ['install'], { cwd: profile, encoding: 'utf8' })
|
|
91
|
+
if (install.status !== 0 || install.error) fail('pnpm install in the scratch profile failed', `${install.stdout}\n${install.stderr}`)
|
|
92
|
+
|
|
93
|
+
// Phase 1 — mount proof: the composed tree must carry the dsh-dcp entry, with
|
|
94
|
+
// compaction-basic disabled rather than removed-by-default.
|
|
95
|
+
const dump = dsh(['--profile', 'smoke', '--dump-config'])
|
|
96
|
+
if (dump.status !== 0 || dump.error) fail('dsh --dump-config failed on the scratch profile', `${dump.stdout}\n${dump.stderr}`)
|
|
97
|
+
if (!dump.stdout.includes(entryId)) {
|
|
98
|
+
fail(`the composed profile tree does not contain the "${entryId}" entry — the patch mount is broken`, dump.stdout)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Phase 2 — boot proof: the plugin tree must LOAD without a loader error.
|
|
102
|
+
const bootSeconds = 25
|
|
103
|
+
const boot = dsh(['--profile', 'smoke'], { timeout: bootSeconds * 1000, killSignal: 'SIGKILL' })
|
|
104
|
+
const output = `${boot.stdout ?? ''}\n${boot.stderr ?? ''}`
|
|
105
|
+
const loaderErrors = [
|
|
106
|
+
/plugin tree failed to load/,
|
|
107
|
+
/failed to apply loader entry/,
|
|
108
|
+
/cannot get property ".*" without inject/,
|
|
109
|
+
/cannot get required service/,
|
|
110
|
+
/Cannot find (package|module)/,
|
|
111
|
+
/unknown key/,
|
|
112
|
+
]
|
|
113
|
+
const hit = loaderErrors.filter((re) => re.test(output))
|
|
114
|
+
if (hit.length > 0) {
|
|
115
|
+
fail('the real host failed to load the plugin tree:', output.split('\n').filter((line) => hit.some((re) => re.test(line)) || /Error/.test(line)).slice(0, 15).join('\n'))
|
|
116
|
+
}
|
|
117
|
+
if (boot.signal !== 'SIGKILL' && boot.status !== 0) {
|
|
118
|
+
fail(`dsh exited early with code ${boot.status} and no loader error — unexpected`, output)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log(`smoke-boot: PASS — ${ownName} mounted via cordis.patch.yml (compaction-basic disabled), composed into the scratch profile tree, and booted clean in real dsh (${boot.signal === 'SIGKILL' ? `survived ${bootSeconds}s boot window` : `exited ${boot.status}`})`)
|
|
122
|
+
rmSync(work, { recursive: true, force: true })
|