fix(dsh): убрать импорты DSH-пакетов из бандла

This commit is contained in:
Andrey Epifancev 2026-10-24 15:31:39 +04:00
parent 7e143be61a
commit 7665218247
7 changed files with 89 additions and 23 deletions

View File

@ -34,7 +34,7 @@
- [x] Bootstrap `using-superpowers`: резолв версии, marker, dedupe, compaction.
- [x] `/role`: парсинг, два слоя, инъекция, followup.
- [x] `dsh/install.sh`, `mcp.codegraph.yml`, README/PORT.
- [x] `bash dsh/tests/run.sh` — 22 теста зелёные.
- [x] `bash dsh/tests/run.sh` — 25 тестов зелёные.
- [x] `bash tests/run.sh` — 630 тестов зелёные (регрессий нет).
- [ ] Живая проверка в DSH: маркер, guard на main, `/role`, acceptance
superpowers.

View File

@ -90,10 +90,14 @@ dsh/
mcp.codegraph.yml # optional CodeGraph MCP row
```
`index.js` is the only file importing DSH packages
(`@deepseek-ai/dsh-llm`, `@deepseek-ai/schemastery`). The pure modules are
testable with plain Node; this is why the port's 22 tests run without a
DSH process.
The bundle imports only Node builtins and files inside this package. It
deliberately does not import `@deepseek-ai/dsh-llm` or
`@deepseek-ai/schemastery`: a local-directory profile install resolves imports
from the profile, not from the DSH installation, so a DSH-internal import fails
at activation with `ERR_MODULE_NOT_FOUND`. `lib/message.js` is a local copy of
the tiny `createUserMessage` constructor, and `Config` was replaced by defaults
normalized in `apply()`. The pure modules are testable with plain Node; this is
why the port's 25 tests run without a DSH process.
### Guard flow
@ -217,6 +221,14 @@ profile was removed; the real `desktop` profile was not touched.
- **DSH is 0.2.1-rc.3.** The plugin/event API is not frozen; pin the version and
re-run the tests after upgrades.
- **Local profile imports are not resolved from the DSH installation.** The
first install attempt failed at activation with
`ERR_MODULE_NOT_FOUND: Cannot find package '@deepseek-ai/schemastery'`. The
bundle now imports only Node builtins and local files: `lib/message.js`
replaces `createUserMessage` from `@deepseek-ai/dsh-llm`, and `Config` was
dropped in favor of defaults normalized in `apply()`. Any future DSH service
must be reached through `ctx` (injected service), never through an
`@deepseek-ai/*` import; a package test enforces this.
- **`ctx.tools.guard` is synchronous.** The guard resolves cwd through
`workingDirectory.get(session)` (sync) and runs the hooks with
`execFileSync`. If a future DSH version changes the guard contract or makes

View File

@ -6,7 +6,7 @@
* the five-line role summary. The command's own result is UI-only.
*/
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { createUserMessage } from './lib/message.js'
import { buildRoleText, listRoles, parseRole } from './lib/role.js'
function currentDirectory(workingDirectory, agent) {

View File

@ -11,37 +11,25 @@
*
* Everything else (CLAUDE.md, roles, OpenSpec, Gitea, codegraph, project
* templates) stays in the repository and is consumed as ordinary files.
*
* The bundle intentionally has no `@deepseek-ai/*` imports: a local-directory
* profile install resolves imports from the profile, not from the DSH
* installation, so only Node builtins and files in this package are safe.
*/
import { existsSync } from 'node:fs'
import { homedir } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { z } from '@deepseek-ai/schemastery'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { registerRoleCommand } from './command-role.js'
import { createGuard } from './lib/guards.js'
import { isBootstrapVisible, prepareBootstrap } from './lib/bootstrap.js'
import { createUserMessage } from './lib/message.js'
import { DEFAULT_ROLE_NAMES } from './lib/role.js'
export const name = 'dsh-harness'
export const inject = ['tools', 'workingDirectory']
export const Config = z.object({
enabled: z.boolean().default(true),
guards: z.boolean().default(true),
bootstrap: z.boolean().default(true),
bootstrapSubagents: z.boolean().default(false),
roleCommand: z.boolean().default(true),
hooksDir: z.string().default(''),
rolesDir: z.string().default(''),
projectRolesDir: z.string().default(''),
roleNames: z.array(z.string()).default(DEFAULT_ROLE_NAMES),
bootstrapSkillPath: z.string().default(''),
toolMappingPath: z.string().default(''),
bootstrapMaxBytes: z.number().default(65_536),
})
/** Resolve the hook directory for both a linked checkout and a packed package. */
function resolveHooksDir(bundleDir, configured) {
if (configured) return configured

28
dsh/bundle/lib/message.js Normal file
View File

@ -0,0 +1,28 @@
/**
* Minimal local copy of the DSH user-message constructor.
*
* The bundle must load in a profile whose module resolution does not include
* the DSH installation's internal packages. `createUserMessage` in
* `@deepseek-ai/dsh-llm` is only `{ ...input, role: "user", id: randomUUID() }`
* plus a deep freeze, so keeping a local copy removes the only runtime import
* outside this package.
*/
import { randomUUID } from 'node:crypto'
function deepFreeze(value) {
if (value === null || typeof value !== 'object' || Object.isFrozen(value)) return value
Object.freeze(value)
for (const child of Object.values(value)) deepFreeze(child)
return value
}
/** Create an immutable user-role message with a fresh identity. */
export function createUserMessage(input) {
const message = {
...structuredClone(input),
role: 'user',
id: randomUUID(),
}
return deepFreeze(message)
}

View File

@ -0,0 +1,24 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { createUserMessage } from '../bundle/lib/message.js'
test('createUserMessage returns a frozen user message with a fresh id', () => {
const message = createUserMessage({
content: [{ type: 'text', text: 'hello' }],
source: { kind: 'user' },
})
assert.equal(message.role, 'user')
assert.match(message.id, /^[0-9a-f-]{36}$/)
assert.deepEqual(message.content, [{ type: 'text', text: 'hello' }])
assert.deepEqual(message.source, { kind: 'user' })
assert.ok(Object.isFrozen(message))
assert.ok(Object.isFrozen(message.content))
assert.ok(Object.isFrozen(message.content[0]))
})
test('createUserMessage does not reuse an id', () => {
const a = createUserMessage({ content: [], source: { kind: 'user' } })
const b = createUserMessage({ content: [], source: { kind: 'user' } })
assert.notEqual(a.id, b.id)
})

View File

@ -50,3 +50,17 @@ test('index.js registers all three integration points', () => {
assert.match(source, /createGuard/)
assert.match(source, /prepareBootstrap/)
})
test('runtime files have no external @deepseek-ai imports', () => {
for (const file of [
'index.js',
'command-role.js',
'lib/message.js',
'lib/guards.js',
'lib/bootstrap.js',
'lib/role.js',
]) {
const source = readFileSync(join(bundleDir, file), 'utf8')
assert.doesNotMatch(source, /from ['"]@deepseek-ai\//, `${file} must not import DSH internals`)
}
})