dsh-harbor-evolution 0.2.0 → 0.3.1
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.md +30 -31
- package/bin/dsh-harbor.mjs +23 -4
- package/lib/setup.js +316 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,71 +1,70 @@
|
|
|
1
1
|
# dsh-harbor-evolution
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Installable DeepSeek Harness Plugin + Skill for running stable Harbor evaluation and controlled Agent evolution loops.
|
|
4
|
+
|
|
5
|
+
The package gives DSH four deterministic Harbor tools and bundles the model- and user-invocable `evolve-agent-with-harbor` Skill. The Skill clarifies the evaluation contract, initializes missing structures, establishes a baseline, diagnoses evidence, limits each iteration to one controlled Candidate change, runs regression evaluation, and produces a Promotion Gate recommendation.
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
Requirements: Docker, Node.js 22+, pnpm, and [uv](https://docs.astral.sh/uv/). Run this from the business Agent workspace:
|
|
8
10
|
|
|
9
11
|
```bash
|
|
10
|
-
|
|
11
|
-
uv pip install --python .venv/bin/python harbor-dsh-evolution
|
|
12
|
-
source .venv/bin/activate
|
|
13
|
-
pnpm dlx @deepseek-ai/dsh@0.1.0-rc.6 plugin --profile web add -w dsh-harbor-evolution@0.2.0
|
|
12
|
+
npx --yes dsh-harbor-evolution@latest setup --project-root "$PWD"
|
|
14
13
|
```
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
The setup command installs both required runtimes:
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
HARBOR_BIN=/absolute/path/to/.venv/bin/harbor \
|
|
21
|
-
HARBOR_DSH_BIN=/absolute/path/to/.venv/bin/harbor-dsh \
|
|
22
|
-
pnpm dlx @deepseek-ai/dsh@0.1.0-rc.6 web
|
|
23
|
-
```
|
|
17
|
+
- `harbor-dsh-evolution==0.3.0` in a managed Python environment.
|
|
18
|
+
- `dsh-harbor-evolution@0.3.0` in the selected DSH profile.
|
|
24
19
|
|
|
25
|
-
|
|
20
|
+
It then stores the absolute Harbor executable paths and `projectRoot` in the profile's `harbor-evolution` block and verifies the integration. Existing unrelated profile entries are preserved, and rerunning setup updates the same block.
|
|
26
21
|
|
|
27
|
-
|
|
22
|
+
The default profile is `web`. Use `--profile headless` only when that is the profile you actually run. See all options with:
|
|
28
23
|
|
|
29
24
|
```bash
|
|
30
|
-
|
|
25
|
+
npx --yes dsh-harbor-evolution@latest setup --help
|
|
31
26
|
```
|
|
32
27
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
The bundle inserts one `harbor-evolution` entry and registers this project's model- and user-invocable Skill through the official DSH Skill Registry:
|
|
36
|
-
|
|
37
|
-
- `evolve-agent-with-harbor`
|
|
38
|
-
|
|
39
|
-
The Skill guides the Agent through workspace inspection, requirements clarification, safe initialization, baseline evaluation, evidence-based diagnosis, one controlled Candidate change, regression comparison, and a Promotion Gate recommendation. Invoke it explicitly from Web or TUI with:
|
|
28
|
+
Stop any old DSH process and run the exact restart command printed by setup. Then invoke:
|
|
40
29
|
|
|
41
30
|
```text
|
|
42
31
|
/evolve-agent-with-harbor
|
|
43
32
|
Inspect this workspace and help me clarify and initialize a stable Harbor self-evolution loop.
|
|
44
33
|
```
|
|
45
34
|
|
|
46
|
-
The
|
|
35
|
+
The Plugin registers:
|
|
47
36
|
|
|
48
37
|
- `harbor_candidate_snapshot`
|
|
49
38
|
- `harbor_eval_run`
|
|
50
39
|
- `harbor_eval_result`
|
|
51
40
|
- `harbor_candidate_compare`
|
|
52
41
|
|
|
53
|
-
The shortest direct evaluation call needs
|
|
42
|
+
The shortest direct evaluation call needs `candidatePath` and `datasetPath`. Prefer the Skill for a new project because it will not run or compare Jobs until the material evaluation contract is resolved.
|
|
54
43
|
|
|
55
|
-
|
|
44
|
+
## What setup writes
|
|
45
|
+
|
|
46
|
+
The selected profile receives one id-targeted override:
|
|
56
47
|
|
|
57
48
|
```yaml
|
|
58
49
|
- id: harbor-evolution
|
|
59
50
|
config:
|
|
60
51
|
projectRoot: /workspace/my-agent
|
|
61
52
|
jobsDir: jobs
|
|
62
|
-
harborBin: /
|
|
63
|
-
harborDshBin: /
|
|
53
|
+
harborBin: /managed/runtime/.venv/bin/harbor
|
|
54
|
+
harborDshBin: /managed/runtime/.venv/bin/harbor-dsh
|
|
64
55
|
pythonPath: ""
|
|
65
56
|
```
|
|
66
57
|
|
|
67
|
-
Keep `pythonPath` empty
|
|
58
|
+
Keep `pythonPath` empty for the published Python package. `candidatePath`, `datasetPath`, `jobPath`, and `policyPath` are constrained to `projectRoot`.
|
|
59
|
+
|
|
60
|
+
For source development from the repository:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
./hse dsh-install-source web
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Do not use `dsh plugin add ./packages/dsh-plugin` directly from a fresh checkout. pnpm records a `link:` dependency, and Node resolves imports from the real checkout path. The source installer first runs the package's locked `npm ci`, then links it and installs the local Python Adapter. Normal users should always use the registry-backed setup command above.
|
|
68
67
|
|
|
69
|
-
|
|
68
|
+
See the [complete DSH Web quickstart](https://github.com/istarwyh/harbor-self-evolving/blob/main/docs/dsh-web-quickstart.md) for UI verification, first evaluation, Candidate comparison, and troubleshooting.
|
|
70
69
|
|
|
71
|
-
The
|
|
70
|
+
The Plugin never deploys a Candidate or mutates the active Champion. Existing CI/CD remains responsible for building, deploying, and promoting the exact evaluated artifact.
|
package/bin/dsh-harbor.mjs
CHANGED
|
@@ -3,14 +3,33 @@
|
|
|
3
3
|
import process from 'node:process'
|
|
4
4
|
import { snapshotCandidate } from '../lib/candidate.js'
|
|
5
5
|
import { runProcess } from '../lib/process.js'
|
|
6
|
+
import { parseSetupArgs, renderSetupResult, setupIntegration } from '../lib/setup.js'
|
|
6
7
|
|
|
7
|
-
function usage() {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
function usage(stream = console.error) {
|
|
9
|
+
stream('Usage: dsh-harbor setup [options]')
|
|
10
|
+
stream(' dsh-harbor snapshot <candidate-dir> [--id <id>] [--version <version>]')
|
|
11
|
+
stream(' dsh-harbor doctor')
|
|
12
|
+
stream('')
|
|
13
|
+
stream('Setup options:')
|
|
14
|
+
stream(' --project-root <path> Agent workspace (default: current directory)')
|
|
15
|
+
stream(' --profile <name> DSH profile (default: web)')
|
|
16
|
+
stream(' --jobs-dir <path> Job directory under projectRoot (default: jobs)')
|
|
17
|
+
stream(' --dsh-home <path> DSH state directory (default: DSH_HOME or ~/.dsh)')
|
|
18
|
+
stream(' --runtime-dir <path> Managed Harbor Python environment')
|
|
10
19
|
}
|
|
11
20
|
|
|
12
21
|
async function main() {
|
|
13
22
|
const [command, ...args] = process.argv.slice(2)
|
|
23
|
+
if (command === 'setup') {
|
|
24
|
+
const options = parseSetupArgs(args)
|
|
25
|
+
if (options.help) {
|
|
26
|
+
usage(console.log)
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
const result = await setupIntegration(options, { onProgress: message => console.log(message) })
|
|
30
|
+
console.log(renderSetupResult(result))
|
|
31
|
+
return
|
|
32
|
+
}
|
|
14
33
|
if (command === 'doctor') {
|
|
15
34
|
const harbor = await runProcess(process.env.HARBOR_BIN || 'harbor', ['--version'], { timeoutMs: 10000 })
|
|
16
35
|
const plugins = await runProcess(process.env.HARBOR_BIN || 'harbor', ['plugins', 'list'], { timeoutMs: 10000 })
|
|
@@ -34,6 +53,6 @@ async function main() {
|
|
|
34
53
|
}
|
|
35
54
|
|
|
36
55
|
main().catch(error => {
|
|
37
|
-
console.error(error.message)
|
|
56
|
+
console.error(error.result?.stderr?.trim() || error.message)
|
|
38
57
|
process.exitCode = 1
|
|
39
58
|
})
|
package/lib/setup.js
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import process from 'node:process'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
import { runProcess } from './process.js'
|
|
8
|
+
|
|
9
|
+
export const DSH_VERSION = '0.1.0-rc.6'
|
|
10
|
+
|
|
11
|
+
const packageJson = JSON.parse(
|
|
12
|
+
await readFile(new URL('../package.json', import.meta.url), 'utf8'),
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
export const INTEGRATION_VERSION = packageJson.version
|
|
16
|
+
|
|
17
|
+
function requireValue(args, index, flag) {
|
|
18
|
+
const value = args[index + 1]
|
|
19
|
+
if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`)
|
|
20
|
+
return value
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function parseSetupArgs(args) {
|
|
24
|
+
const options = {}
|
|
25
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
26
|
+
const flag = args[index]
|
|
27
|
+
if (flag === '--help' || flag === '-h') {
|
|
28
|
+
options.help = true
|
|
29
|
+
continue
|
|
30
|
+
}
|
|
31
|
+
if (flag === '--profile') options.profile = requireValue(args, index++, flag)
|
|
32
|
+
else if (flag === '--project-root') options.projectRoot = requireValue(args, index++, flag)
|
|
33
|
+
else if (flag === '--jobs-dir') options.jobsDir = requireValue(args, index++, flag)
|
|
34
|
+
else if (flag === '--dsh-home') options.dshHome = requireValue(args, index++, flag)
|
|
35
|
+
else if (flag === '--runtime-dir') options.runtimeDir = requireValue(args, index++, flag)
|
|
36
|
+
else if (flag === '--plugin-spec') options.pluginSpec = requireValue(args, index++, flag)
|
|
37
|
+
else if (flag === '--python-spec') options.pythonSpec = requireValue(args, index++, flag)
|
|
38
|
+
else throw new Error(`Unknown setup option: ${flag}`)
|
|
39
|
+
}
|
|
40
|
+
return options
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function executableInVenv(venvDir, name, platform) {
|
|
44
|
+
return platform === 'win32'
|
|
45
|
+
? path.join(venvDir, 'Scripts', `${name}.exe`)
|
|
46
|
+
: path.join(venvDir, 'bin', name)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function assertSafeProfile(profile) {
|
|
50
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(profile)) {
|
|
51
|
+
throw new Error('profile may contain only letters, numbers, dot, underscore, and hyphen')
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function assertJobsDir(projectRoot, jobsDir) {
|
|
56
|
+
if (!jobsDir) throw new Error('jobsDir cannot be empty')
|
|
57
|
+
const root = path.resolve(projectRoot)
|
|
58
|
+
const resolved = path.resolve(root, jobsDir)
|
|
59
|
+
if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) {
|
|
60
|
+
throw new Error('jobsDir must stay under projectRoot')
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function resolveSetupOptions(raw = {}, environment = {}) {
|
|
65
|
+
const env = environment.env ?? process.env
|
|
66
|
+
const cwd = environment.cwd ?? process.cwd()
|
|
67
|
+
const home = environment.home ?? os.homedir()
|
|
68
|
+
const platform = environment.platform ?? process.platform
|
|
69
|
+
const profile = raw.profile ?? 'web'
|
|
70
|
+
const projectRoot = path.resolve(cwd, raw.projectRoot ?? '.')
|
|
71
|
+
const dataHome = env.XDG_DATA_HOME
|
|
72
|
+
? path.resolve(env.XDG_DATA_HOME)
|
|
73
|
+
: path.join(home, '.local', 'share')
|
|
74
|
+
const dshHome = path.resolve(raw.dshHome ?? env.DSH_HOME ?? path.join(home, '.dsh'))
|
|
75
|
+
const runtimeDir = path.resolve(raw.runtimeDir ?? path.join(dataHome, 'harbor-dsh-evolution'))
|
|
76
|
+
const jobsDir = raw.jobsDir ?? 'jobs'
|
|
77
|
+
const venvDir = path.join(runtimeDir, '.venv')
|
|
78
|
+
|
|
79
|
+
assertSafeProfile(profile)
|
|
80
|
+
assertJobsDir(projectRoot, jobsDir)
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
profile,
|
|
84
|
+
projectRoot,
|
|
85
|
+
jobsDir,
|
|
86
|
+
dshHome,
|
|
87
|
+
runtimeDir,
|
|
88
|
+
venvDir,
|
|
89
|
+
pythonBin: executableInVenv(venvDir, 'python', platform),
|
|
90
|
+
harborBin: executableInVenv(venvDir, 'harbor', platform),
|
|
91
|
+
harborDshBin: executableInVenv(venvDir, 'harbor-dsh', platform),
|
|
92
|
+
patchFile: path.join(dshHome, 'profiles', profile, 'cordis.patch.yml'),
|
|
93
|
+
pluginSpec: raw.pluginSpec ?? `dsh-harbor-evolution@${INTEGRATION_VERSION}`,
|
|
94
|
+
pythonSpec: raw.pythonSpec ?? `harbor-dsh-evolution==${INTEGRATION_VERSION}`,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function resolveLocalPluginDirectory(pluginSpec, cwd = process.cwd()) {
|
|
99
|
+
let candidate
|
|
100
|
+
if (pluginSpec.startsWith('file://')) candidate = fileURLToPath(pluginSpec)
|
|
101
|
+
else if (pluginSpec.startsWith('file:')) candidate = path.resolve(cwd, pluginSpec.slice(5))
|
|
102
|
+
else if (!pluginSpec.startsWith('github:') && !pluginSpec.startsWith('git+')) {
|
|
103
|
+
candidate = path.resolve(cwd, pluginSpec)
|
|
104
|
+
}
|
|
105
|
+
if (!candidate) return undefined
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const details = await stat(candidate)
|
|
109
|
+
if (!details.isDirectory()) return undefined
|
|
110
|
+
const manifest = JSON.parse(await readFile(path.join(candidate, 'package.json'), 'utf8'))
|
|
111
|
+
return manifest.name === 'dsh-harbor-evolution' ? candidate : undefined
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error.code === 'ENOENT' || error instanceof SyntaxError) return undefined
|
|
114
|
+
throw error
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function quoted(value) {
|
|
119
|
+
return JSON.stringify(value)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function renderHarborProfileEntry(config) {
|
|
123
|
+
return [
|
|
124
|
+
'- id: harbor-evolution',
|
|
125
|
+
' config:',
|
|
126
|
+
` projectRoot: ${quoted(config.projectRoot)}`,
|
|
127
|
+
` jobsDir: ${quoted(config.jobsDir)}`,
|
|
128
|
+
` harborBin: ${quoted(config.harborBin)}`,
|
|
129
|
+
` harborDshBin: ${quoted(config.harborDshBin)}`,
|
|
130
|
+
' pythonPath: ""',
|
|
131
|
+
].join('\n')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function upsertHarborProfileEntry(source, config) {
|
|
135
|
+
const entry = renderHarborProfileEntry(config)
|
|
136
|
+
const lines = source.replace(/\r\n/g, '\n').split('\n')
|
|
137
|
+
const starts = []
|
|
138
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
139
|
+
if (/^- id:\s*["']?harbor-evolution["']?\s*$/.test(lines[index])) starts.push(index)
|
|
140
|
+
}
|
|
141
|
+
if (starts.length > 1) {
|
|
142
|
+
throw new Error('cordis.patch.yml contains multiple harbor-evolution entries; remove duplicates first')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (starts.length === 1) {
|
|
146
|
+
const start = starts[0]
|
|
147
|
+
let end = lines.length
|
|
148
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
149
|
+
if (/^- \S/.test(lines[index])) {
|
|
150
|
+
end = index
|
|
151
|
+
break
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
lines.splice(start, end - start, ...entry.split('\n'))
|
|
155
|
+
return `${lines.join('\n').replace(/\n+$/g, '')}\n`
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const emptyArrayIndex = lines.findIndex(line => line.trim() === '[]')
|
|
159
|
+
const payload = lines.filter(line => line.trim() && !line.trim().startsWith('#'))
|
|
160
|
+
if (emptyArrayIndex >= 0 && payload.length === 1) {
|
|
161
|
+
lines.splice(emptyArrayIndex, 1, ...entry.split('\n'))
|
|
162
|
+
return `${lines.join('\n').replace(/\n+$/g, '')}\n`
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const current = lines.join('\n').replace(/\n+$/g, '')
|
|
166
|
+
return `${current ? `${current}\n` : ''}${entry}\n`
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function writeProfilePatch(file, config) {
|
|
170
|
+
let source
|
|
171
|
+
try {
|
|
172
|
+
source = await readFile(file, 'utf8')
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (error.code !== 'ENOENT') throw error
|
|
175
|
+
source = [
|
|
176
|
+
'# Harbor Self-Evolving profile overrides.',
|
|
177
|
+
'# This file is applied after the DSH bundle layers.',
|
|
178
|
+
'[]',
|
|
179
|
+
'',
|
|
180
|
+
].join('\n')
|
|
181
|
+
}
|
|
182
|
+
const updated = upsertHarborProfileEntry(source, config)
|
|
183
|
+
if (updated === source) return false
|
|
184
|
+
|
|
185
|
+
await mkdir(path.dirname(file), { recursive: true })
|
|
186
|
+
const temporary = `${file}.${process.pid}.tmp`
|
|
187
|
+
await writeFile(temporary, updated, 'utf8')
|
|
188
|
+
await rename(temporary, file)
|
|
189
|
+
return true
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function assertDirectory(directory) {
|
|
193
|
+
let details
|
|
194
|
+
try {
|
|
195
|
+
details = await stat(directory)
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (error.code === 'ENOENT') throw new Error(`projectRoot does not exist: ${directory}`)
|
|
198
|
+
throw error
|
|
199
|
+
}
|
|
200
|
+
if (!details.isDirectory()) throw new Error(`projectRoot is not a directory: ${directory}`)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function requireCommand(run, command) {
|
|
204
|
+
try {
|
|
205
|
+
return await run(command, ['--version'], { timeoutMs: 10_000 })
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (error.code === 'ENOENT' || error.cause?.code === 'ENOENT') {
|
|
208
|
+
throw new Error(`Missing ${command}; install it before running setup`)
|
|
209
|
+
}
|
|
210
|
+
throw error
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function processFailure(error) {
|
|
215
|
+
const detail = error.result?.stderr?.trim() || error.result?.stdout?.trim()
|
|
216
|
+
return detail ? `${error.message}\n${detail}` : error.message
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function setupIntegration(raw = {}, dependencies = {}) {
|
|
220
|
+
const config = resolveSetupOptions(raw, dependencies)
|
|
221
|
+
const run = dependencies.run ?? runProcess
|
|
222
|
+
const progress = dependencies.onProgress ?? (() => {})
|
|
223
|
+
const env = dependencies.env ?? process.env
|
|
224
|
+
const warnings = []
|
|
225
|
+
|
|
226
|
+
const nodeMajor = Number.parseInt(process.versions.node.split('.')[0], 10)
|
|
227
|
+
if (nodeMajor < 22) throw new Error(`Node.js 22 or newer is required; found ${process.version}`)
|
|
228
|
+
await assertDirectory(config.projectRoot)
|
|
229
|
+
|
|
230
|
+
progress('1/4 Checking uv, pnpm, and Docker...')
|
|
231
|
+
await requireCommand(run, 'uv')
|
|
232
|
+
await requireCommand(run, 'pnpm')
|
|
233
|
+
try {
|
|
234
|
+
await run('docker', ['info'], { timeoutMs: 15_000 })
|
|
235
|
+
} catch (error) {
|
|
236
|
+
warnings.push(`Docker is not ready; installation can finish, but Harbor Jobs will fail until it is available. ${processFailure(error)}`)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const localPluginDir = await resolveLocalPluginDirectory(
|
|
240
|
+
config.pluginSpec,
|
|
241
|
+
dependencies.cwd ?? process.cwd(),
|
|
242
|
+
)
|
|
243
|
+
if (localPluginDir) {
|
|
244
|
+
progress('Preparing dependencies for the linked DSH plugin checkout...')
|
|
245
|
+
await requireCommand(run, 'npm')
|
|
246
|
+
// Node resolves a symlinked package from its real checkout path. Install
|
|
247
|
+
// the complete locked graph there so runtime dependencies and host peers
|
|
248
|
+
// do not disappear behind the profile's `link:` entry.
|
|
249
|
+
await run('npm', ['ci', '--ignore-scripts'], { cwd: localPluginDir })
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
progress('2/4 Installing the Harbor Python runtime...')
|
|
253
|
+
await mkdir(config.runtimeDir, { recursive: true })
|
|
254
|
+
await run('uv', ['venv', '--python', '3.12', '--allow-existing', config.venvDir])
|
|
255
|
+
await run('uv', [
|
|
256
|
+
'pip', 'install',
|
|
257
|
+
'--python', config.pythonBin,
|
|
258
|
+
'--refresh-package', 'harbor-dsh-evolution',
|
|
259
|
+
config.pythonSpec,
|
|
260
|
+
])
|
|
261
|
+
|
|
262
|
+
progress(`3/4 Installing the DSH bundle into profile ${config.profile}...`)
|
|
263
|
+
try {
|
|
264
|
+
await run('pnpm', [
|
|
265
|
+
'--silent', 'dlx', `@deepseek-ai/dsh@${DSH_VERSION}`,
|
|
266
|
+
'plugin', '--profile', config.profile, 'add', '-w', '--save-exact', config.pluginSpec,
|
|
267
|
+
], { env: { ...env, DSH_HOME: config.dshHome } })
|
|
268
|
+
} catch (error) {
|
|
269
|
+
throw new Error(processFailure(error))
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
progress('4/4 Saving paths and verifying the integration...')
|
|
273
|
+
const patchChanged = await writeProfilePatch(config.patchFile, config)
|
|
274
|
+
const harborVersion = await run(config.harborBin, ['--version'], { timeoutMs: 10_000 })
|
|
275
|
+
const plugins = await run(config.harborBin, ['plugins', 'list'], { timeoutMs: 10_000 })
|
|
276
|
+
if (!plugins.stdout.includes('dsh-evolution')) {
|
|
277
|
+
throw new Error('Harbor installed, but its dsh-evolution plugin entry point was not discovered')
|
|
278
|
+
}
|
|
279
|
+
await run(config.harborDshBin, ['--help'], { timeoutMs: 10_000 })
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
...config,
|
|
283
|
+
localPluginDir,
|
|
284
|
+
patchChanged,
|
|
285
|
+
harborVersion: harborVersion.stdout.trim() || harborVersion.stderr.trim(),
|
|
286
|
+
warnings,
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function shellQuote(value) {
|
|
291
|
+
return `'${String(value).replaceAll("'", `'"'"'`)}'`
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function renderSetupResult(result) {
|
|
295
|
+
const startCommand = result.profile === 'web'
|
|
296
|
+
? `DSH_HOME=${shellQuote(result.dshHome)} pnpm dlx @deepseek-ai/dsh@${DSH_VERSION} web`
|
|
297
|
+
: `DSH_HOME=${shellQuote(result.dshHome)} pnpm dlx @deepseek-ai/dsh@${DSH_VERSION} --profile ${shellQuote(result.profile)}`
|
|
298
|
+
const lines = [
|
|
299
|
+
'',
|
|
300
|
+
`Installed Harbor Self-Evolving ${INTEGRATION_VERSION}.`,
|
|
301
|
+
`DSH profile: ${result.profile}`,
|
|
302
|
+
`Project root: ${result.projectRoot}`,
|
|
303
|
+
`Profile config: ${result.patchFile}`,
|
|
304
|
+
`Harbor: ${result.harborVersion}`,
|
|
305
|
+
]
|
|
306
|
+
for (const warning of result.warnings) lines.push(`Warning: ${warning}`)
|
|
307
|
+
lines.push(
|
|
308
|
+
'',
|
|
309
|
+
'Restart DSH from the Agent workspace:',
|
|
310
|
+
`cd ${shellQuote(result.projectRoot)}`,
|
|
311
|
+
startCommand,
|
|
312
|
+
'',
|
|
313
|
+
'Then invoke: /evolve-agent-with-harbor',
|
|
314
|
+
)
|
|
315
|
+
return lines.join('\n')
|
|
316
|
+
}
|