@foggy-projects/deepseek-harness-plugin 0.4.0-beta.6 → 0.4.0-beta.8
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 +32 -6
- package/THIRD-PARTY-RUNTIME-NOTICES.md +16 -0
- package/docs/PUBLIC-BETA-READINESS.md +51 -0
- package/docs/WINDOWS-BETA-ACCEPTANCE.md +71 -0
- package/experience/linux/README.md +5 -6
- package/experience/linux/prepare.sh +7 -9
- package/lib/client.js +132 -6
- package/lib/index.js +157 -28
- package/lib/python-runtime.js +339 -0
- package/lib/remote-descriptor.js +1 -0
- package/lib/version.js +5 -0
- package/package.json +6 -3
- package/skills/foggy-deepseek-onboarding/SKILL.md +24 -5
- package/skills/foggy-deepseek-onboarding/assets/onboarding-state.schema.json +20 -0
- package/skills/foggy-deepseek-onboarding/assets/versions.json +46 -2
- package/skills/foggy-deepseek-onboarding/references/onboarding-workflow.md +18 -4
- package/skills/foggy-deepseek-onboarding/scripts/doctor.ps1 +2 -2
- package/skills/foggy-deepseek-onboarding/scripts/doctor.sh +1 -2
- package/skills/foggy-deepseek-onboarding/scripts/install.ps1 +2 -2
- package/skills/foggy-deepseek-onboarding/scripts/install.sh +1 -2
- package/skills/foggy-deepseek-onboarding/scripts/invoke-onboarding.ps1 +30 -0
- package/skills/foggy-deepseek-onboarding/scripts/invoke-onboarding.sh +24 -0
- package/skills/foggy-deepseek-onboarding/scripts/onboard.ps1 +2 -2
- package/skills/foggy-deepseek-onboarding/scripts/onboard.sh +1 -2
- package/skills/foggy-deepseek-onboarding/scripts/onboarding.py +400 -43
- package/skills/foggy-deepseek-onboarding/scripts/runtime-start.ps1 +2 -2
- package/skills/foggy-deepseek-onboarding/scripts/runtime-start.sh +1 -2
- package/skills/foggy-deepseek-onboarding/scripts/runtime-stop.ps1 +2 -2
- package/skills/foggy-deepseek-onboarding/scripts/runtime-stop.sh +1 -2
- package/skills/foggy-deepseek-onboarding/scripts/uninstall.ps1 +2 -2
- package/skills/foggy-deepseek-onboarding/scripts/uninstall.sh +1 -2
package/lib/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process'
|
|
2
|
-
import { access, readFile } from 'node:fs/promises'
|
|
2
|
+
import { access, mkdir, readFile, rename, writeFile } from 'node:fs/promises'
|
|
3
3
|
import { constants as fsConstants } from 'node:fs'
|
|
4
4
|
import { dirname, join, delimiter } from 'node:path'
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
6
6
|
import { promisify } from 'node:util'
|
|
7
7
|
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
|
8
8
|
import { createFoggySkillProvider } from './skill-provider.js'
|
|
9
|
-
import {
|
|
9
|
+
import { ensurePythonRuntime, probePythonRuntime } from './python-runtime.js'
|
|
10
|
+
import { compatible, compatibleNode } from './version.js'
|
|
10
11
|
|
|
11
12
|
const execFileAsync = promisify(execFile)
|
|
12
13
|
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
@@ -71,16 +72,6 @@ async function commandVersion(command, args) {
|
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
async function pythonCommand() {
|
|
75
|
-
const configured = process.env.FOGGY_PYTHON
|
|
76
|
-
const candidates = configured ? [configured] : process.platform === 'win32' ? ['python', 'py'] : ['python3', 'python']
|
|
77
|
-
for (const command of candidates) {
|
|
78
|
-
const probe = await commandVersion(command, ['--version'])
|
|
79
|
-
if (probe.available) return command
|
|
80
|
-
}
|
|
81
|
-
throw new Error('Python 3.11+ was not found; set FOGGY_PYTHON to its executable')
|
|
82
|
-
}
|
|
83
|
-
|
|
84
75
|
function parseOutput(stdout, stderr) {
|
|
85
76
|
const text = String(stdout ?? '').trim()
|
|
86
77
|
if (!text) throw new Error(String(stderr ?? '').trim() || 'Foggy onboarding returned no JSON')
|
|
@@ -91,14 +82,34 @@ function parseOutput(stdout, stderr) {
|
|
|
91
82
|
}
|
|
92
83
|
}
|
|
93
84
|
|
|
94
|
-
async function runOnboarding(args, timeout = 15 * 60_000) {
|
|
95
|
-
const
|
|
85
|
+
async function runOnboarding(args, timeout = 15 * 60_000, options = {}) {
|
|
86
|
+
const roots = defaultRoots()
|
|
87
|
+
const manifest = await readJson(versionsFile)
|
|
88
|
+
const cacheDirs = (process.env.FOGGY_ASSET_CACHE_DIRS || '').split(delimiter).filter(Boolean)
|
|
89
|
+
const python = options.ensurePython
|
|
90
|
+
? await ensurePythonRuntime({
|
|
91
|
+
installRoot: roots.installRoot,
|
|
92
|
+
manifest,
|
|
93
|
+
cacheDirs,
|
|
94
|
+
force: options.forcePython,
|
|
95
|
+
onProgress: options.onPythonProgress,
|
|
96
|
+
})
|
|
97
|
+
: await probePythonRuntime({ installRoot: roots.installRoot, manifest })
|
|
98
|
+
if (!python.available) {
|
|
99
|
+
throw new Error('Foggy managed Python is unavailable; initialize or repair the Python component')
|
|
100
|
+
}
|
|
101
|
+
await options.flushPythonProgress?.()
|
|
96
102
|
try {
|
|
97
|
-
const { stdout, stderr } = await execFileAsync(python, [onboardingScript, ...args], {
|
|
103
|
+
const { stdout, stderr } = await execFileAsync(python.path, [onboardingScript, ...args], {
|
|
98
104
|
windowsHide: true,
|
|
99
105
|
timeout,
|
|
100
106
|
maxBuffer: 4 * 1024 * 1024,
|
|
101
107
|
cwd: process.env.FOGGY_PROJECT_ROOT || process.cwd(),
|
|
108
|
+
env: {
|
|
109
|
+
...process.env,
|
|
110
|
+
FOGGY_ONBOARDING_PYTHON: python.path,
|
|
111
|
+
FOGGY_ONBOARDING_PYTHON_SOURCE: python.source,
|
|
112
|
+
},
|
|
102
113
|
})
|
|
103
114
|
return parseOutput(stdout, stderr)
|
|
104
115
|
} catch (error) {
|
|
@@ -107,6 +118,51 @@ async function runOnboarding(args, timeout = 15 * 60_000) {
|
|
|
107
118
|
}
|
|
108
119
|
}
|
|
109
120
|
|
|
121
|
+
async function assertSystemPrerequisites(kind) {
|
|
122
|
+
if (kind === 'initialize' && !compatibleNode(process.versions.node)) {
|
|
123
|
+
throw new Error(`DeepSeek Harness requires Node.js ^22.19.0 or >=24.0.0; detected ${process.versions.node}`)
|
|
124
|
+
}
|
|
125
|
+
if (kind === 'initialize' || kind === 'runtime-start') {
|
|
126
|
+
const java = compatible(await commandVersion(process.env.JAVA_EXE || 'java', ['-version']), '17.0')
|
|
127
|
+
if (!java.available) throw new Error('Java 17+ is required; install a system JRE/JDK or set JAVA_EXE')
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function writeJsonAtomic(path, payload) {
|
|
132
|
+
await mkdir(dirname(path), { recursive: true })
|
|
133
|
+
const temporary = `${path}.${process.pid}.${Date.now()}.tmp`
|
|
134
|
+
await writeFile(temporary, `${JSON.stringify(payload, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 })
|
|
135
|
+
await rename(temporary, path)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function createPythonProgressReporter(operation, path) {
|
|
139
|
+
let pending = Promise.resolve()
|
|
140
|
+
return {
|
|
141
|
+
update(detail) {
|
|
142
|
+
const fraction = Math.max(0, Math.min(1, Number(detail.fraction) || 0))
|
|
143
|
+
const payload = {
|
|
144
|
+
schemaVersion: 'foggy-deepseek-onboarding-progress/v1',
|
|
145
|
+
operationId: operation.id,
|
|
146
|
+
kind: operation.kind,
|
|
147
|
+
state: 'running',
|
|
148
|
+
phase: 'python',
|
|
149
|
+
message: detail.message || 'Preparing managed Python',
|
|
150
|
+
currentFile: detail.currentFile || null,
|
|
151
|
+
percent: Math.round(((1 + fraction) / 7) * 100),
|
|
152
|
+
step: { index: 2, total: 7 },
|
|
153
|
+
startedAt: operation.startedAt,
|
|
154
|
+
updatedAt: new Date().toISOString(),
|
|
155
|
+
}
|
|
156
|
+
if (detail.bytes) payload.bytes = detail.bytes
|
|
157
|
+
operation.progress = payload
|
|
158
|
+
pending = pending.then(() => writeJsonAtomic(path, payload))
|
|
159
|
+
},
|
|
160
|
+
flush() {
|
|
161
|
+
return pending
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
110
166
|
function processRunning(pid) {
|
|
111
167
|
if (!Number.isInteger(pid) || pid <= 0) return false
|
|
112
168
|
try {
|
|
@@ -145,18 +201,22 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
|
|
|
145
201
|
const runtimeStatePath = join(roots.dataRoot, 'runtime-state.json')
|
|
146
202
|
const progressPath = join(roots.dataRoot, 'operation-progress.json')
|
|
147
203
|
const manifest = await readJson(versionsFile)
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
pythonProbe = await commandVersion(await pythonCommand(), ['--version'])
|
|
151
|
-
} catch (error) {
|
|
152
|
-
pythonProbe = { available: false, output: '', error: String(error.message ?? error) }
|
|
153
|
-
}
|
|
154
|
-
const python = compatible(pythonProbe, '3.11')
|
|
204
|
+
const python = await probePythonRuntime({ installRoot: roots.installRoot, manifest })
|
|
155
205
|
const java = compatible(await commandVersion(process.env.JAVA_EXE || 'java', ['-version']), '17.0')
|
|
156
206
|
let state = null
|
|
157
207
|
let runtime = null
|
|
158
208
|
try { state = await readJson(statePath) } catch {}
|
|
159
209
|
try { runtime = await readJson(runtimeStatePath) } catch {}
|
|
210
|
+
let onboarding = { success: true, profiles: [], profileCount: 0 }
|
|
211
|
+
let profileMigration = { success: true, entries: [], pendingCount: 0, conflictCount: 0, profileStore: roots.profileStore }
|
|
212
|
+
if (state) {
|
|
213
|
+
try { onboarding = await runOnboarding(['onboard-list']) } catch (error) {
|
|
214
|
+
onboarding = { success: false, profiles: [], profileCount: 0, error: String(error.message ?? error) }
|
|
215
|
+
}
|
|
216
|
+
try { profileMigration = await runOnboarding(['profile-migration-status']) } catch (error) {
|
|
217
|
+
profileMigration = { success: false, entries: [], pendingCount: 0, conflictCount: 0, profileStore: roots.profileStore, error: String(error.message ?? error) }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
160
220
|
const cliPath = state?.cli?.command
|
|
161
221
|
const launcherPath = state?.launcher?.path
|
|
162
222
|
const analysisSkillPath = state?.skills?.analysis?.path || join(roots.installRoot, 'skills', 'foggy-ai-analysis')
|
|
@@ -187,7 +247,8 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
|
|
|
187
247
|
provider: 'foggy-managed-skills',
|
|
188
248
|
},
|
|
189
249
|
}
|
|
190
|
-
const installed = components.
|
|
250
|
+
const installed = components.python.available
|
|
251
|
+
&& components.cli.installed
|
|
191
252
|
&& components.launcher.installed
|
|
192
253
|
&& components.analysisSkill.installed
|
|
193
254
|
&& components.onboardingSkill.installed
|
|
@@ -218,6 +279,8 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
|
|
|
218
279
|
roots,
|
|
219
280
|
components,
|
|
220
281
|
operation,
|
|
282
|
+
onboarding,
|
|
283
|
+
profileMigration,
|
|
221
284
|
next: installed ? (running ? 'configure-database' : 'start-runtime') : 'initialize',
|
|
222
285
|
}
|
|
223
286
|
}
|
|
@@ -231,6 +294,7 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
|
|
|
231
294
|
workspaceMode: 'dsh-session-cwd',
|
|
232
295
|
versions: Object.fromEntries(Object.entries(manifest.components).map(([name, value]) => [name, value.version])),
|
|
233
296
|
operations: [
|
|
297
|
+
'download and verify a pinned private Python runtime',
|
|
234
298
|
'create isolated Python environment',
|
|
235
299
|
'download and verify pinned CLI and Launcher assets',
|
|
236
300
|
'install the Foggy analysis Skill into the global managed component directory',
|
|
@@ -249,6 +313,56 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
|
|
|
249
313
|
return this.startOperation('repair', true)
|
|
250
314
|
}
|
|
251
315
|
|
|
316
|
+
async repairCli() {
|
|
317
|
+
return this.startOperation('repair-cli', false, ['install', '--repair-component', 'cli'])
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async repairPython() {
|
|
321
|
+
return this.startOperation('repair-python', false, ['install'])
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async repairLauncher() {
|
|
325
|
+
return this.startOperation('repair-launcher', false, ['install', '--repair-component', 'launcher'])
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async repairAnalysisSkill() {
|
|
329
|
+
return this.startOperation('repair-analysis-skill', true, ['install', '--repair-component', 'analysis-skill'])
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async migrateProfiles() {
|
|
333
|
+
return this.startOperation('profile-migration', false, ['profile-migrate', '--approve'])
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async diagnostics() {
|
|
337
|
+
const roots = defaultRoots()
|
|
338
|
+
const status = await this.status()
|
|
339
|
+
let doctor
|
|
340
|
+
try {
|
|
341
|
+
doctor = await runOnboarding(['doctor', '--no-fail'])
|
|
342
|
+
} catch (error) {
|
|
343
|
+
doctor = { success: false, error: String(error.message ?? error), bootstrapOnly: true }
|
|
344
|
+
}
|
|
345
|
+
const report = {
|
|
346
|
+
schemaVersion: 'foggy-deepseek-diagnostics/v1',
|
|
347
|
+
generatedAt: new Date().toISOString(),
|
|
348
|
+
packageVersion: status.packageVersion,
|
|
349
|
+
state: status.state,
|
|
350
|
+
installed: status.installed,
|
|
351
|
+
running: status.running,
|
|
352
|
+
runtimeUrl: status.runtimeUrl,
|
|
353
|
+
roots: status.roots,
|
|
354
|
+
components: status.components,
|
|
355
|
+
onboarding: status.onboarding,
|
|
356
|
+
profileMigration: status.profileMigration,
|
|
357
|
+
doctor,
|
|
358
|
+
}
|
|
359
|
+
const directory = join(roots.dataRoot, 'diagnostics')
|
|
360
|
+
await mkdir(directory, { recursive: true })
|
|
361
|
+
const path = join(directory, `diagnostics-${Date.now()}.json`)
|
|
362
|
+
await writeFile(path, `${JSON.stringify(report, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 })
|
|
363
|
+
return { success: true, path, report }
|
|
364
|
+
}
|
|
365
|
+
|
|
252
366
|
async runtimeStart() {
|
|
253
367
|
return this.startOperation('runtime-start', false, ['runtime-start'])
|
|
254
368
|
}
|
|
@@ -263,7 +377,8 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
|
|
|
263
377
|
}
|
|
264
378
|
const id = `${Date.now()}-${Math.random().toString(16).slice(2, 10)}`
|
|
265
379
|
const args = explicitArgs ?? ['install']
|
|
266
|
-
|
|
380
|
+
const reportsProgress = args[0] === 'install'
|
|
381
|
+
if (reportsProgress) {
|
|
267
382
|
const roots = defaultRoots()
|
|
268
383
|
args.push('--progress-file', join(roots.dataRoot, 'operation-progress.json'), '--operation-id', id, '--operation-kind', kind)
|
|
269
384
|
}
|
|
@@ -279,7 +394,7 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
|
|
|
279
394
|
finishedAt: null,
|
|
280
395
|
result: null,
|
|
281
396
|
error: null,
|
|
282
|
-
progress:
|
|
397
|
+
progress: reportsProgress ? {
|
|
283
398
|
schemaVersion: 'foggy-deepseek-onboarding-progress/v1',
|
|
284
399
|
operationId: id,
|
|
285
400
|
kind,
|
|
@@ -288,11 +403,22 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
|
|
|
288
403
|
message: 'Starting Foggy initialization',
|
|
289
404
|
currentFile: null,
|
|
290
405
|
percent: 0,
|
|
291
|
-
step: { index: 1, total:
|
|
406
|
+
step: { index: 1, total: 7 },
|
|
292
407
|
} : null,
|
|
293
408
|
promise: null,
|
|
294
409
|
}
|
|
295
|
-
|
|
410
|
+
const pythonProgress = reportsProgress
|
|
411
|
+
? createPythonProgressReporter(operation, join(defaultRoots().dataRoot, 'operation-progress.json'))
|
|
412
|
+
: null
|
|
413
|
+
operation.promise = (async () => {
|
|
414
|
+
await assertSystemPrerequisites(kind)
|
|
415
|
+
return runOnboarding(args, 15 * 60_000, {
|
|
416
|
+
ensurePython: true,
|
|
417
|
+
forcePython: kind === 'repair-python',
|
|
418
|
+
onPythonProgress: (detail) => pythonProgress?.update(detail),
|
|
419
|
+
flushPythonProgress: () => pythonProgress?.flush(),
|
|
420
|
+
})
|
|
421
|
+
})()
|
|
296
422
|
.then((result) => {
|
|
297
423
|
operation.state = result.success === false ? 'failed' : 'succeeded'
|
|
298
424
|
operation.result = result
|
|
@@ -327,7 +453,10 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
|
|
|
327
453
|
}
|
|
328
454
|
|
|
329
455
|
const markerInitializers = []
|
|
330
|
-
for (const method of [
|
|
456
|
+
for (const method of [
|
|
457
|
+
'status', 'plan', 'initialize', 'repair', 'repairPython', 'repairCli', 'repairLauncher', 'repairAnalysisSkill',
|
|
458
|
+
'migrateProfiles', 'diagnostics', 'runtimeStart', 'runtimeStop',
|
|
459
|
+
]) {
|
|
331
460
|
Remote(method)(FoggyIntegrationGateway.prototype[method], {
|
|
332
461
|
kind: 'method',
|
|
333
462
|
name: method,
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import {
|
|
4
|
+
access,
|
|
5
|
+
copyFile,
|
|
6
|
+
mkdir,
|
|
7
|
+
open,
|
|
8
|
+
readFile,
|
|
9
|
+
rename,
|
|
10
|
+
rm,
|
|
11
|
+
stat,
|
|
12
|
+
writeFile,
|
|
13
|
+
} from 'node:fs/promises'
|
|
14
|
+
import { constants as fsConstants } from 'node:fs'
|
|
15
|
+
import { dirname, isAbsolute, join } from 'node:path'
|
|
16
|
+
import { promisify } from 'node:util'
|
|
17
|
+
import { compatible, versionParts } from './version.js'
|
|
18
|
+
|
|
19
|
+
const execFileAsync = promisify(execFile)
|
|
20
|
+
const MARKER_SCHEMA = 'foggy-managed-python/v1'
|
|
21
|
+
|
|
22
|
+
async function exists(path) {
|
|
23
|
+
try {
|
|
24
|
+
await access(path, fsConstants.F_OK)
|
|
25
|
+
return true
|
|
26
|
+
} catch {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function commandVersion(command) {
|
|
32
|
+
try {
|
|
33
|
+
const { stdout, stderr } = await execFileAsync(command, ['--version'], {
|
|
34
|
+
windowsHide: true,
|
|
35
|
+
timeout: 15_000,
|
|
36
|
+
maxBuffer: 256 * 1024,
|
|
37
|
+
})
|
|
38
|
+
return {
|
|
39
|
+
available: true,
|
|
40
|
+
output: `${stdout}\n${stderr}`.trim().split(/\r?\n/)[0] ?? '',
|
|
41
|
+
command,
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return { available: false, output: '', command, error: error.code ?? 'COMMAND_FAILED' }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function sha256(path) {
|
|
49
|
+
const file = await open(path, 'r')
|
|
50
|
+
const hash = createHash('sha256')
|
|
51
|
+
try {
|
|
52
|
+
for await (const chunk of file.readableWebStream()) hash.update(Buffer.from(chunk))
|
|
53
|
+
} finally {
|
|
54
|
+
await file.close()
|
|
55
|
+
}
|
|
56
|
+
return hash.digest('hex')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function pythonComponent(manifest) {
|
|
60
|
+
const component = manifest?.components?.python
|
|
61
|
+
if (!component?.version || !component?.assets) {
|
|
62
|
+
throw new Error('The Foggy component manifest does not define a managed Python runtime')
|
|
63
|
+
}
|
|
64
|
+
return component
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function pythonAssetKey(platform = process.platform, arch = process.arch) {
|
|
68
|
+
return `${platform}-${arch}`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function selectPythonAsset(manifest, platform = process.platform, arch = process.arch) {
|
|
72
|
+
const component = pythonComponent(manifest)
|
|
73
|
+
const key = pythonAssetKey(platform, arch)
|
|
74
|
+
const asset = component.assets[key]
|
|
75
|
+
if (!asset) {
|
|
76
|
+
throw new Error(`Managed Python is not published for ${platform}/${arch}; set FOGGY_PYTHON to a compatible Python executable`)
|
|
77
|
+
}
|
|
78
|
+
if (!asset.file || !asset.url || !/^[a-f0-9]{64}$/.test(asset.sha256 || '')) {
|
|
79
|
+
throw new Error(`Managed Python asset ${key} is incomplete or has an invalid SHA256`)
|
|
80
|
+
}
|
|
81
|
+
return { ...asset, key }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function managedPythonHome(installRoot, manifest) {
|
|
85
|
+
return join(installRoot, 'python', pythonComponent(manifest).version)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function managedPythonExecutable(installRoot, manifest, platform = process.platform) {
|
|
89
|
+
const home = managedPythonHome(installRoot, manifest)
|
|
90
|
+
return platform === 'win32' ? join(home, 'python.exe') : join(home, 'bin', 'python3')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function markerPath(installRoot, manifest) {
|
|
94
|
+
return join(managedPythonHome(installRoot, manifest), '.foggy-managed-python.json')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function readMarker(installRoot, manifest) {
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(await readFile(markerPath(installRoot, manifest), 'utf8'))
|
|
100
|
+
} catch {
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function exactVersion(probe, expected) {
|
|
106
|
+
const actualParts = versionParts(probe.output)
|
|
107
|
+
const expectedParts = versionParts(expected)
|
|
108
|
+
return expectedParts.every((part, index) => actualParts[index] === part)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function probePythonRuntime({ installRoot, manifest, env = process.env } = {}) {
|
|
112
|
+
const minimum = manifest.components.cli.minimumPythonVersion
|
|
113
|
+
if (env.FOGGY_PYTHON) {
|
|
114
|
+
const probe = compatible(await commandVersion(env.FOGGY_PYTHON), minimum)
|
|
115
|
+
const isPython = /^Python\s+\d+/i.test(probe.output)
|
|
116
|
+
return {
|
|
117
|
+
...probe,
|
|
118
|
+
available: probe.available && isPython,
|
|
119
|
+
source: 'override',
|
|
120
|
+
managed: false,
|
|
121
|
+
path: env.FOGGY_PYTHON,
|
|
122
|
+
version: probe.available && isPython ? versionParts(probe.output).slice(0, 3).join('.') : null,
|
|
123
|
+
error: probe.available && !isPython ? 'Configured executable is not Python' : probe.error,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const component = pythonComponent(manifest)
|
|
128
|
+
const command = managedPythonExecutable(installRoot, manifest)
|
|
129
|
+
const marker = await readMarker(installRoot, manifest)
|
|
130
|
+
if (!marker || marker.schemaVersion !== MARKER_SCHEMA || marker.version !== component.version) {
|
|
131
|
+
return {
|
|
132
|
+
available: false,
|
|
133
|
+
detected: false,
|
|
134
|
+
output: '',
|
|
135
|
+
error: 'Managed Python is not installed or its marker is invalid',
|
|
136
|
+
source: 'managed',
|
|
137
|
+
managed: true,
|
|
138
|
+
path: command,
|
|
139
|
+
version: component.version,
|
|
140
|
+
minimum,
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const probe = compatible(await commandVersion(command), minimum)
|
|
144
|
+
const valid = probe.available && exactVersion(probe, component.version)
|
|
145
|
+
return {
|
|
146
|
+
...probe,
|
|
147
|
+
available: valid,
|
|
148
|
+
detected: probe.detected,
|
|
149
|
+
...(valid ? {} : { error: probe.error || `Expected Python ${component.version}` }),
|
|
150
|
+
source: 'managed',
|
|
151
|
+
managed: true,
|
|
152
|
+
path: command,
|
|
153
|
+
home: managedPythonHome(installRoot, manifest),
|
|
154
|
+
version: component.version,
|
|
155
|
+
asset: marker.asset,
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function safeRenameCorrupt(path) {
|
|
160
|
+
if (!await exists(path)) return null
|
|
161
|
+
const corrupt = `${path}.corrupt-${Date.now()}`
|
|
162
|
+
await rename(path, corrupt)
|
|
163
|
+
return corrupt
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function cachedAsset(asset, cacheDirs) {
|
|
167
|
+
for (const directory of cacheDirs) {
|
|
168
|
+
const candidate = join(directory, asset.file)
|
|
169
|
+
try {
|
|
170
|
+
if ((await stat(candidate)).isFile() && await sha256(candidate) === asset.sha256) return candidate
|
|
171
|
+
} catch {}
|
|
172
|
+
}
|
|
173
|
+
return null
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function materializeAsset(asset, destination, cacheDirs, onProgress) {
|
|
177
|
+
await mkdir(dirname(destination), { recursive: true })
|
|
178
|
+
if (await exists(destination) && await sha256(destination) === asset.sha256) {
|
|
179
|
+
onProgress?.({ fraction: 0.82, message: 'Using verified managed Python archive', currentFile: asset.file })
|
|
180
|
+
return { path: destination, source: 'existing' }
|
|
181
|
+
}
|
|
182
|
+
await safeRenameCorrupt(destination)
|
|
183
|
+
const temporary = `${destination}.download`
|
|
184
|
+
const cached = await cachedAsset(asset, cacheDirs)
|
|
185
|
+
if (cached) {
|
|
186
|
+
onProgress?.({ fraction: 0.35, message: 'Copying managed Python from verified cache', currentFile: asset.file })
|
|
187
|
+
await rm(temporary, { force: true })
|
|
188
|
+
await copyFile(cached, temporary)
|
|
189
|
+
} else {
|
|
190
|
+
let resumeBytes = 0
|
|
191
|
+
try {
|
|
192
|
+
resumeBytes = (await stat(temporary)).size
|
|
193
|
+
} catch {}
|
|
194
|
+
if (resumeBytes >= Number(asset.size || Number.MAX_SAFE_INTEGER)) {
|
|
195
|
+
if (await sha256(temporary) === asset.sha256) {
|
|
196
|
+
await rename(temporary, destination)
|
|
197
|
+
return { path: destination, source: 'resumed' }
|
|
198
|
+
}
|
|
199
|
+
await rm(temporary, { force: true })
|
|
200
|
+
resumeBytes = 0
|
|
201
|
+
}
|
|
202
|
+
const controller = new AbortController()
|
|
203
|
+
const timeout = setTimeout(() => controller.abort(), 20 * 60_000)
|
|
204
|
+
try {
|
|
205
|
+
const headers = resumeBytes > 0 ? { Range: `bytes=${resumeBytes}-` } : undefined
|
|
206
|
+
const response = await fetch(asset.url, { redirect: 'follow', signal: controller.signal, headers })
|
|
207
|
+
if (!response.ok || !response.body) throw new Error(`Managed Python download failed: HTTP ${response.status}`)
|
|
208
|
+
const resumed = resumeBytes > 0 && response.status === 206
|
|
209
|
+
if (!resumed) {
|
|
210
|
+
await rm(temporary, { force: true })
|
|
211
|
+
resumeBytes = 0
|
|
212
|
+
}
|
|
213
|
+
const expectedSize = Number(asset.size || (Number(response.headers.get('content-length')) + resumeBytes) || 0)
|
|
214
|
+
const file = await open(temporary, resumed ? 'a' : 'w')
|
|
215
|
+
let received = resumeBytes
|
|
216
|
+
try {
|
|
217
|
+
const reader = response.body.getReader()
|
|
218
|
+
while (true) {
|
|
219
|
+
const { done, value } = await reader.read()
|
|
220
|
+
if (done) break
|
|
221
|
+
const chunk = Buffer.from(value)
|
|
222
|
+
await file.write(chunk)
|
|
223
|
+
received += chunk.length
|
|
224
|
+
const fraction = expectedSize > 0 ? Math.min(received / expectedSize, 1) : 0
|
|
225
|
+
onProgress?.({
|
|
226
|
+
fraction: 0.05 + fraction * 0.72,
|
|
227
|
+
message: resumed ? 'Resuming managed Python download' : 'Downloading managed Python',
|
|
228
|
+
currentFile: asset.file,
|
|
229
|
+
bytes: { received, total: expectedSize || null },
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
await file.sync()
|
|
233
|
+
} finally {
|
|
234
|
+
await file.close()
|
|
235
|
+
}
|
|
236
|
+
} finally {
|
|
237
|
+
clearTimeout(timeout)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (await sha256(temporary) !== asset.sha256) {
|
|
241
|
+
await rm(temporary, { force: true })
|
|
242
|
+
throw new Error(`Managed Python SHA256 mismatch for ${asset.file}`)
|
|
243
|
+
}
|
|
244
|
+
await rename(temporary, destination)
|
|
245
|
+
return { path: destination, source: cached ? 'cache' : 'download' }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function validateArchiveEntries(text) {
|
|
249
|
+
const entries = text.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean)
|
|
250
|
+
if (!entries.length) throw new Error('Managed Python archive is empty')
|
|
251
|
+
for (const entry of entries) {
|
|
252
|
+
const normalized = entry.replaceAll('\\', '/')
|
|
253
|
+
if (isAbsolute(entry) || normalized.startsWith('/') || normalized.includes('../') || !normalized.startsWith('python/')) {
|
|
254
|
+
throw new Error(`Managed Python archive contains an unsafe entry: ${entry}`)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function extractArchive(archive, installRoot, manifest, asset, onProgress) {
|
|
260
|
+
const pythonRoot = join(installRoot, 'python')
|
|
261
|
+
const target = managedPythonHome(installRoot, manifest)
|
|
262
|
+
const staging = join(pythonRoot, `.staging-${process.pid}-${Date.now()}`)
|
|
263
|
+
const backup = `${target}.previous-${Date.now()}`
|
|
264
|
+
await mkdir(staging, { recursive: true })
|
|
265
|
+
try {
|
|
266
|
+
onProgress?.({ fraction: 0.84, message: 'Inspecting managed Python archive', currentFile: asset.file })
|
|
267
|
+
const listed = await execFileAsync('tar', ['-tzf', archive], {
|
|
268
|
+
windowsHide: true,
|
|
269
|
+
timeout: 2 * 60_000,
|
|
270
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
271
|
+
})
|
|
272
|
+
validateArchiveEntries(listed.stdout)
|
|
273
|
+
onProgress?.({ fraction: 0.88, message: 'Extracting managed Python', currentFile: asset.file })
|
|
274
|
+
await execFileAsync('tar', ['-xzf', archive, '-C', staging], {
|
|
275
|
+
windowsHide: true,
|
|
276
|
+
timeout: 5 * 60_000,
|
|
277
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
278
|
+
})
|
|
279
|
+
const extracted = join(staging, 'python')
|
|
280
|
+
const executable = process.platform === 'win32' ? join(extracted, 'python.exe') : join(extracted, 'bin', 'python3')
|
|
281
|
+
if (!await exists(executable)) throw new Error('Managed Python archive did not contain the expected interpreter')
|
|
282
|
+
await writeFile(join(extracted, '.foggy-managed-python.json'), `${JSON.stringify({
|
|
283
|
+
schemaVersion: MARKER_SCHEMA,
|
|
284
|
+
version: pythonComponent(manifest).version,
|
|
285
|
+
distribution: pythonComponent(manifest).distribution,
|
|
286
|
+
buildRelease: pythonComponent(manifest).buildRelease,
|
|
287
|
+
platform: process.platform,
|
|
288
|
+
arch: process.arch,
|
|
289
|
+
asset: { file: asset.file, sha256: asset.sha256 },
|
|
290
|
+
installedAt: new Date().toISOString(),
|
|
291
|
+
}, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 })
|
|
292
|
+
|
|
293
|
+
let movedPrevious = false
|
|
294
|
+
if (await exists(target)) {
|
|
295
|
+
await rename(target, backup)
|
|
296
|
+
movedPrevious = true
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
await rename(extracted, target)
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if (movedPrevious && !await exists(target)) await rename(backup, target)
|
|
302
|
+
throw error
|
|
303
|
+
}
|
|
304
|
+
if (movedPrevious) await rm(backup, { recursive: true, force: true })
|
|
305
|
+
} finally {
|
|
306
|
+
await rm(staging, { recursive: true, force: true })
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export async function ensurePythonRuntime({
|
|
311
|
+
installRoot,
|
|
312
|
+
manifest,
|
|
313
|
+
cacheDirs = [],
|
|
314
|
+
force = false,
|
|
315
|
+
env = process.env,
|
|
316
|
+
onProgress,
|
|
317
|
+
} = {}) {
|
|
318
|
+
if (env.FOGGY_PYTHON) {
|
|
319
|
+
const override = await probePythonRuntime({ installRoot, manifest, env })
|
|
320
|
+
if (!override.available) throw new Error(`FOGGY_PYTHON is not compatible: ${override.path}`)
|
|
321
|
+
onProgress?.({ fraction: 1, message: 'Using FOGGY_PYTHON override', currentFile: override.path })
|
|
322
|
+
return override
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const current = await probePythonRuntime({ installRoot, manifest, env })
|
|
326
|
+
if (current.available && !force) {
|
|
327
|
+
onProgress?.({ fraction: 1, message: 'Managed Python is ready', currentFile: current.path })
|
|
328
|
+
return current
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const asset = selectPythonAsset(manifest)
|
|
332
|
+
const archive = join(installRoot, 'downloads', 'python', asset.file)
|
|
333
|
+
const materialized = await materializeAsset(asset, archive, cacheDirs, onProgress)
|
|
334
|
+
await extractArchive(materialized.path, installRoot, manifest, asset, onProgress)
|
|
335
|
+
const installed = await probePythonRuntime({ installRoot, manifest, env })
|
|
336
|
+
if (!installed.available) throw new Error(installed.error || 'Managed Python verification failed')
|
|
337
|
+
onProgress?.({ fraction: 1, message: 'Managed Python is ready', currentFile: installed.path })
|
|
338
|
+
return installed
|
|
339
|
+
}
|
package/lib/remote-descriptor.js
CHANGED
package/lib/version.js
CHANGED
|
@@ -13,3 +13,8 @@ export function compatible(probe, minimum) {
|
|
|
13
13
|
}
|
|
14
14
|
return { ...probe, detected: true, available: true, minimum }
|
|
15
15
|
}
|
|
16
|
+
|
|
17
|
+
export function compatibleNode(version) {
|
|
18
|
+
const [major, minor] = String(version).split('.').map(Number)
|
|
19
|
+
return (major === 22 && minor >= 19) || major >= 24
|
|
20
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foggy-projects/deepseek-harness-plugin",
|
|
3
|
-
"version": "0.4.0-beta.
|
|
3
|
+
"version": "0.4.0-beta.8",
|
|
4
4
|
"description": "Foggy Java data analysis engine integration for DeepSeek Harness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -19,11 +19,14 @@
|
|
|
19
19
|
"!skills/**/*.pyc",
|
|
20
20
|
"cordis.patch.yml",
|
|
21
21
|
"experience/linux/**",
|
|
22
|
+
"docs/PUBLIC-BETA-READINESS.md",
|
|
23
|
+
"docs/WINDOWS-BETA-ACCEPTANCE.md",
|
|
24
|
+
"THIRD-PARTY-RUNTIME-NOTICES.md",
|
|
22
25
|
"README.md"
|
|
23
26
|
],
|
|
24
27
|
"license": "Apache-2.0",
|
|
25
28
|
"engines": {
|
|
26
|
-
"node": "
|
|
29
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
27
30
|
},
|
|
28
31
|
"dsh": {
|
|
29
32
|
"bundle": {
|
|
@@ -54,7 +57,7 @@
|
|
|
54
57
|
"@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2"
|
|
55
58
|
},
|
|
56
59
|
"scripts": {
|
|
57
|
-
"check": "node --check lib/index.js && node --check lib/skill-provider.js && node --check lib/client.js && node --check lib/typert.js && node --check lib/remote.js",
|
|
60
|
+
"check": "node --check lib/index.js && node --check lib/python-runtime.js && node --check lib/skill-provider.js && node --check lib/client.js && node --check lib/typert.js && node --check lib/remote.js",
|
|
58
61
|
"test": "node --test test/package.test.js && python test/onboarding_unit.py"
|
|
59
62
|
}
|
|
60
63
|
}
|