@researai/deepscientist 1.5.1 → 1.5.3
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 +69 -1
- package/bin/ds.js +2239 -153
- package/docs/en/00_QUICK_START.md +60 -20
- package/docs/en/01_SETTINGS_REFERENCE.md +20 -20
- package/docs/en/02_START_RESEARCH_GUIDE.md +11 -11
- package/docs/en/03_QQ_CONNECTOR_GUIDE.md +10 -10
- package/docs/en/05_TUI_GUIDE.md +1 -1
- package/docs/en/09_DOCTOR.md +48 -4
- package/docs/en/90_ARCHITECTURE.md +4 -2
- package/docs/zh/00_QUICK_START.md +60 -20
- package/docs/zh/01_SETTINGS_REFERENCE.md +21 -21
- package/docs/zh/02_START_RESEARCH_GUIDE.md +19 -19
- package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +10 -10
- package/docs/zh/05_TUI_GUIDE.md +1 -1
- package/docs/zh/09_DOCTOR.md +46 -4
- package/install.sh +125 -8
- package/package.json +2 -1
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +6 -1
- package/src/deepscientist/artifact/service.py +553 -26
- package/src/deepscientist/bash_exec/monitor.py +23 -4
- package/src/deepscientist/bash_exec/runtime.py +3 -0
- package/src/deepscientist/bash_exec/service.py +132 -4
- package/src/deepscientist/bridges/base.py +10 -19
- package/src/deepscientist/channels/discord_gateway.py +25 -2
- package/src/deepscientist/channels/feishu_long_connection.py +41 -3
- package/src/deepscientist/channels/qq.py +524 -64
- package/src/deepscientist/channels/qq_gateway.py +22 -3
- package/src/deepscientist/channels/relay.py +429 -90
- package/src/deepscientist/channels/slack_socket.py +29 -5
- package/src/deepscientist/channels/telegram_polling.py +25 -2
- package/src/deepscientist/channels/whatsapp_local_session.py +32 -4
- package/src/deepscientist/cli.py +27 -0
- package/src/deepscientist/config/models.py +6 -40
- package/src/deepscientist/config/service.py +165 -156
- package/src/deepscientist/connector_profiles.py +346 -0
- package/src/deepscientist/connector_runtime.py +88 -43
- package/src/deepscientist/daemon/api/handlers.py +65 -11
- package/src/deepscientist/daemon/api/router.py +4 -2
- package/src/deepscientist/daemon/app.py +772 -219
- package/src/deepscientist/doctor.py +69 -2
- package/src/deepscientist/gitops/diff.py +3 -0
- package/src/deepscientist/home.py +25 -2
- package/src/deepscientist/mcp/context.py +3 -1
- package/src/deepscientist/mcp/server.py +66 -7
- package/src/deepscientist/migration.py +114 -0
- package/src/deepscientist/prompts/builder.py +71 -3
- package/src/deepscientist/qq_profiles.py +186 -0
- package/src/deepscientist/quest/layout.py +1 -0
- package/src/deepscientist/quest/service.py +70 -12
- package/src/deepscientist/quest/stage_views.py +46 -0
- package/src/deepscientist/runners/codex.py +2 -0
- package/src/deepscientist/shared.py +44 -17
- package/src/prompts/connectors/lingzhu.md +3 -0
- package/src/prompts/connectors/qq.md +42 -2
- package/src/prompts/system.md +123 -10
- package/src/skills/analysis-campaign/SKILL.md +35 -6
- package/src/skills/baseline/SKILL.md +73 -32
- package/src/skills/decision/SKILL.md +4 -3
- package/src/skills/experiment/SKILL.md +28 -6
- package/src/skills/finalize/SKILL.md +5 -2
- package/src/skills/idea/SKILL.md +2 -2
- package/src/skills/intake-audit/SKILL.md +2 -2
- package/src/skills/rebuttal/SKILL.md +4 -2
- package/src/skills/review/SKILL.md +4 -2
- package/src/skills/scout/SKILL.md +2 -2
- package/src/skills/write/SKILL.md +2 -2
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-w5lF2Ttt.js → AiManusChatView-qzChi9uh.js} +67 -94
- package/src/ui/dist/assets/{AnalysisPlugin-DJOED79I.js → AnalysisPlugin-CcC_-UqN.js} +1 -1
- package/src/ui/dist/assets/{AutoFigurePlugin-DaG61Y0M.js → AutoFigurePlugin-DD8LkJLe.js} +5 -5
- package/src/ui/dist/assets/{CliPlugin-CV4LqUB_.js → CliPlugin-DJJFfVmW.js} +17 -110
- package/src/ui/dist/assets/{CodeEditorPlugin-DylfAea4.js → CodeEditorPlugin-CrjkHNLh.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-F7saY0LM.js → CodeViewerPlugin-obnD6G5R.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-COP0c7jf.js → DocViewerPlugin-DB9SUQVd.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-CAS05pT9.js → GitDiffViewerPlugin-DZLlNlD2.js} +1 -1
- package/src/ui/dist/assets/{ImageViewerPlugin-Bco1CN_w.js → ImageViewerPlugin-BGwfDZ0Y.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-CvMlCD99.js → LabCopilotPanel-dfLptQcR.js} +10 -10
- package/src/ui/dist/assets/{LabPlugin-BYankkE4.js → LabPlugin-CeGjAl3A.js} +1 -1
- package/src/ui/dist/assets/{LatexPlugin-LDSMR-t-.js → LatexPlugin-BBJ7kd1V.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-B7o80jgm.js → MarkdownViewerPlugin-DKZi7BcB.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-CM6ZOcpC.js → MarketplacePlugin-C_k-9jD0.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-Dc61cXmK.js → NotebookEditor-4R88_BMO.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-DWowuQwx.js → PdfLoader-DwEFQLrw.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-BsJM1q_a.js → PdfMarkdownPlugin-D-jdsqF8.js} +3 -3
- package/src/ui/dist/assets/{PdfViewerPlugin-DB2eEEFQ.js → PdfViewerPlugin-CmeBGDY0.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-CraThSvt.js → SearchPlugin-Dlz2WKJ4.js} +1 -1
- package/src/ui/dist/assets/{Stepper-CgocRTPq.js → Stepper-ClOgzWM3.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-B1JGhKtd.js → TextViewerPlugin-DDQWxibk.js} +4 -4
- package/src/ui/dist/assets/{VNCViewer-CclFC7FM.js → VNCViewer-CJXT0Nm8.js} +9 -9
- package/src/ui/dist/assets/{bibtex-D3IKsMl7.js → bibtex-DLr4Rtk4.js} +1 -1
- package/src/ui/dist/assets/{code-BP37Xx0p.js → code-DgKK408Y.js} +1 -1
- package/src/ui/dist/assets/{file-content-BAJSu-9r.js → file-content-6HBqQnvQ.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-DUGeCTuy.js → file-diff-panel-Dhu0TbBM.js} +1 -1
- package/src/ui/dist/assets/{file-socket-CXc1Ojf7.js → file-socket-CP3iwVZG.js} +1 -1
- package/src/ui/dist/assets/{file-utils-2J21jt7M.js → file-utils-BsS-Aw68.js} +1 -1
- package/src/ui/dist/assets/{image-CMMmgvcn.js → image-ByeK-Zcv.js} +1 -1
- package/src/ui/dist/assets/{index-DmwmJmbW.js → index-BLjo5--a.js} +33610 -31016
- package/src/ui/dist/assets/{index-CWgMgpow.js → index-BdsE0uRz.js} +11 -11
- package/src/ui/dist/assets/{index-s7aHnNQ4.js → index-C-eX-N6A.js} +1 -1
- package/src/ui/dist/assets/{index-KGt-z-dD.css → index-CuQhlrR-.css} +2747 -2
- package/src/ui/dist/assets/{index-BaVumsQT.js → index-DyremSIv.js} +2 -2
- package/src/ui/dist/assets/{message-square-CQRfX0Am.js → message-square-DnagiLnc.js} +1 -1
- package/src/ui/dist/assets/{monaco-B4TbdsrF.js → monaco-4kBFeprs.js} +1 -1
- package/src/ui/dist/assets/{popover-B8Rokodk.js → popover-hRCXZzs2.js} +1 -1
- package/src/ui/dist/assets/{project-sync-D_i96KH4.js → project-sync-O_85YuP6.js} +1 -1
- package/src/ui/dist/assets/{sigma-D12PnzCN.js → sigma-DvKopSnL.js} +1 -1
- package/src/ui/dist/assets/{tooltip-B6YrI4aJ.js → tooltip-BmlPc6kc.js} +1 -1
- package/src/ui/dist/assets/{trash-Bc8jGp0V.js → trash-n-UvdZFR.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-mXVCYSZ-.js → useCliAccess-WDd3_wIh.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-Bg6b9H9K.js → useFileDiffOverlay-rXLIL2NF.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-Drh5GEnL.js → wrap-text-qIYQ4a_W.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-CJj9DZLn.js → zoom-out-fZXCEFsy.js} +1 -1
- package/src/ui/dist/index.html +2 -2
- package/uv.lock +1155 -0
- package/src/ui/dist/assets/LabPlugin-D9jVIo0A.css +0 -2698
package/bin/ds.js
CHANGED
|
@@ -3,13 +3,17 @@ const crypto = require('node:crypto');
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const os = require('node:os');
|
|
5
5
|
const path = require('node:path');
|
|
6
|
+
const readline = require('node:readline');
|
|
6
7
|
const { pathToFileURL } = require('node:url');
|
|
7
8
|
const { spawn, spawnSync } = require('node:child_process');
|
|
8
9
|
|
|
9
10
|
const repoRoot = path.resolve(__dirname, '..');
|
|
10
|
-
const srcPath = path.join(repoRoot, 'src');
|
|
11
11
|
const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
|
|
12
|
+
const pyprojectToml = fs.readFileSync(path.join(repoRoot, 'pyproject.toml'), 'utf8');
|
|
12
13
|
const pythonCandidates = process.platform === 'win32' ? ['python', 'py'] : ['python3', 'python'];
|
|
14
|
+
const requiredPythonSpec = parseRequiredPythonSpec(pyprojectToml);
|
|
15
|
+
const minimumPythonVersion = parseMinimumPythonVersion(requiredPythonSpec);
|
|
16
|
+
const launcherWrapperCommands = ['ds', 'ds-cli', 'research', 'resear'];
|
|
13
17
|
const pythonCommands = new Set([
|
|
14
18
|
'init',
|
|
15
19
|
'new',
|
|
@@ -29,6 +33,9 @@ const pythonCommands = new Set([
|
|
|
29
33
|
'latex',
|
|
30
34
|
'config',
|
|
31
35
|
]);
|
|
36
|
+
const UPDATE_PACKAGE_NAME = String(packageJson.name || '@researai/deepscientist').trim() || '@researai/deepscientist';
|
|
37
|
+
const UPDATE_CHECK_TTL_MS = 12 * 60 * 60 * 1000;
|
|
38
|
+
const UPDATE_PROMPT_TTL_MS = 12 * 60 * 60 * 1000;
|
|
32
39
|
|
|
33
40
|
const optionsWithValues = new Set(['--home', '--host', '--port', '--quest-id', '--mode']);
|
|
34
41
|
|
|
@@ -37,14 +44,51 @@ function printLauncherHelp() {
|
|
|
37
44
|
|
|
38
45
|
Usage:
|
|
39
46
|
ds
|
|
47
|
+
ds update
|
|
48
|
+
ds update --check
|
|
49
|
+
ds update --yes
|
|
50
|
+
ds migrate /data/DeepScientist
|
|
51
|
+
ds --hero
|
|
52
|
+
ds --hero doctor
|
|
40
53
|
ds --tui
|
|
41
54
|
ds --both
|
|
55
|
+
ds --host 0.0.0.0 --port 21000
|
|
42
56
|
ds --stop
|
|
43
57
|
ds --restart
|
|
58
|
+
ds --status
|
|
44
59
|
ds doctor
|
|
45
60
|
ds latex status
|
|
46
61
|
ds --home ~/DeepScientist --port 20999
|
|
47
62
|
|
|
63
|
+
Launcher flags:
|
|
64
|
+
--host <host> Bind host for the local web daemon
|
|
65
|
+
--port <port> Bind port for the local web daemon
|
|
66
|
+
--tui Start the terminal workspace only
|
|
67
|
+
--both Start web + terminal workspace together
|
|
68
|
+
--no-browser Do not auto-open the browser
|
|
69
|
+
--daemon-only Start the managed daemon and exit
|
|
70
|
+
--status Print managed daemon health as JSON
|
|
71
|
+
--stop Stop the managed daemon
|
|
72
|
+
--restart Restart the managed daemon
|
|
73
|
+
--home <path> Use a custom DeepScientist home
|
|
74
|
+
--hero Use the current working directory as DeepScientist home
|
|
75
|
+
--quest-id <id> Open the TUI on one quest directly
|
|
76
|
+
|
|
77
|
+
Update:
|
|
78
|
+
ds update Check the npm package version and offer update actions
|
|
79
|
+
ds update --check Print structured update status
|
|
80
|
+
ds update --yes Install the latest npm release immediately
|
|
81
|
+
|
|
82
|
+
Migration:
|
|
83
|
+
ds migrate <target> Move the DeepScientist home/install root to a new absolute path
|
|
84
|
+
ds migrate <target> --yes --restart
|
|
85
|
+
|
|
86
|
+
Runtime:
|
|
87
|
+
DeepScientist uses uv to manage a locked local Python runtime.
|
|
88
|
+
If uv is missing, ds bootstraps a local copy under the DeepScientist home automatically.
|
|
89
|
+
If an active conda environment provides Python ${requiredPythonSpec}, ds prefers it.
|
|
90
|
+
Otherwise uv provisions a managed Python under the DeepScientist home automatically.
|
|
91
|
+
|
|
48
92
|
Advanced Python CLI:
|
|
49
93
|
ds init
|
|
50
94
|
ds new "reproduce baseline and test one stronger idea"
|
|
@@ -58,14 +102,306 @@ function ensureDir(targetPath) {
|
|
|
58
102
|
fs.mkdirSync(targetPath, { recursive: true });
|
|
59
103
|
}
|
|
60
104
|
|
|
105
|
+
function expandUserPath(rawPath) {
|
|
106
|
+
const normalized = String(rawPath || '').trim();
|
|
107
|
+
if (!normalized) {
|
|
108
|
+
return normalized;
|
|
109
|
+
}
|
|
110
|
+
if (normalized === '~') {
|
|
111
|
+
return os.homedir();
|
|
112
|
+
}
|
|
113
|
+
if (normalized.startsWith(`~${path.sep}`) || normalized.startsWith('~/')) {
|
|
114
|
+
return path.join(os.homedir(), normalized.slice(2));
|
|
115
|
+
}
|
|
116
|
+
return normalized;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function updateStatePath(home) {
|
|
120
|
+
return path.join(home, 'runtime', 'update-state.json');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function readUpdateState(home) {
|
|
124
|
+
return readJsonFile(updateStatePath(home)) || {};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function writeUpdateState(home, payload) {
|
|
128
|
+
const statePath = updateStatePath(home);
|
|
129
|
+
ensureDir(path.dirname(statePath));
|
|
130
|
+
fs.writeFileSync(statePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function mergeUpdateState(home, patch) {
|
|
134
|
+
const current = readUpdateState(home);
|
|
135
|
+
const next = {
|
|
136
|
+
...current,
|
|
137
|
+
...patch,
|
|
138
|
+
};
|
|
139
|
+
writeUpdateState(home, next);
|
|
140
|
+
return next;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function parseTimestamp(value) {
|
|
144
|
+
if (!value) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const parsed = Date.parse(String(value));
|
|
148
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isExpired(value, ttlMs) {
|
|
152
|
+
const parsed = parseTimestamp(value);
|
|
153
|
+
if (parsed === null) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
return Date.now() - parsed > ttlMs;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function normalizeVersion(value) {
|
|
160
|
+
return String(value || '')
|
|
161
|
+
.trim()
|
|
162
|
+
.replace(/^v/i, '');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function compareVersions(left, right) {
|
|
166
|
+
const leftParts = normalizeVersion(left).split('.').map((item) => Number.parseInt(item, 10) || 0);
|
|
167
|
+
const rightParts = normalizeVersion(right).split('.').map((item) => Number.parseInt(item, 10) || 0);
|
|
168
|
+
const length = Math.max(leftParts.length, rightParts.length, 3);
|
|
169
|
+
for (let index = 0; index < length; index += 1) {
|
|
170
|
+
const leftValue = leftParts[index] || 0;
|
|
171
|
+
const rightValue = rightParts[index] || 0;
|
|
172
|
+
if (leftValue > rightValue) {
|
|
173
|
+
return 1;
|
|
174
|
+
}
|
|
175
|
+
if (leftValue < rightValue) {
|
|
176
|
+
return -1;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function detectInstallMode(rootPath = repoRoot) {
|
|
183
|
+
const normalized = String(rootPath || '');
|
|
184
|
+
return normalized.includes(`${path.sep}node_modules${path.sep}`) ? 'npm-package' : 'source-checkout';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function updateManualCommand(installMode) {
|
|
188
|
+
if (installMode === 'npm-package') {
|
|
189
|
+
return `npm install -g ${UPDATE_PACKAGE_NAME}@latest`;
|
|
190
|
+
}
|
|
191
|
+
return 'git pull && bash install.sh';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function updateSupportSummary(installMode, npmBinary, launcherPath) {
|
|
195
|
+
if (!npmBinary) {
|
|
196
|
+
return {
|
|
197
|
+
canCheck: false,
|
|
198
|
+
canSelfUpdate: false,
|
|
199
|
+
reason: '`npm` is not available on PATH.',
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (installMode !== 'npm-package') {
|
|
203
|
+
return {
|
|
204
|
+
canCheck: true,
|
|
205
|
+
canSelfUpdate: false,
|
|
206
|
+
reason: 'This DeepScientist installation comes from a source checkout and should be updated from Git.',
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (!launcherPath || !fs.existsSync(launcherPath)) {
|
|
210
|
+
return {
|
|
211
|
+
canCheck: true,
|
|
212
|
+
canSelfUpdate: false,
|
|
213
|
+
reason: 'The launcher entrypoint could not be resolved.',
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
canCheck: true,
|
|
218
|
+
canSelfUpdate: true,
|
|
219
|
+
reason: null,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function resolveNpmBinary() {
|
|
224
|
+
return resolveExecutableOnPath(process.platform === 'win32' ? 'npm.cmd' : 'npm') || resolveExecutableOnPath('npm');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function resolveLauncherPath() {
|
|
228
|
+
const configured = String(process.env.DEEPSCIENTIST_LAUNCHER_PATH || '').trim();
|
|
229
|
+
if (configured && fs.existsSync(configured)) {
|
|
230
|
+
return configured;
|
|
231
|
+
}
|
|
232
|
+
const candidate = path.join(repoRoot, 'bin', 'ds.js');
|
|
233
|
+
return fs.existsSync(candidate) ? candidate : null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function fetchLatestPublishedVersion({ npmBinary, timeoutMs = 3500 }) {
|
|
237
|
+
if (!npmBinary) {
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
error: '`npm` is not available on PATH.',
|
|
241
|
+
latestVersion: null,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const result = spawnSync(npmBinary, ['view', UPDATE_PACKAGE_NAME, 'version', '--json'], {
|
|
245
|
+
encoding: 'utf8',
|
|
246
|
+
env: process.env,
|
|
247
|
+
timeout: timeoutMs,
|
|
248
|
+
});
|
|
249
|
+
if (result.error) {
|
|
250
|
+
return {
|
|
251
|
+
ok: false,
|
|
252
|
+
error: result.error.message,
|
|
253
|
+
latestVersion: null,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (result.status !== 0) {
|
|
257
|
+
return {
|
|
258
|
+
ok: false,
|
|
259
|
+
error: (result.stderr || result.stdout || '').trim() || `npm exited with status ${result.status}`,
|
|
260
|
+
latestVersion: null,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
const parsed = JSON.parse(String(result.stdout || 'null'));
|
|
265
|
+
const latestVersion = Array.isArray(parsed) ? normalizeVersion(parsed[parsed.length - 1]) : normalizeVersion(parsed);
|
|
266
|
+
if (!latestVersion) {
|
|
267
|
+
throw new Error('npm returned an empty version string.');
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
ok: true,
|
|
271
|
+
error: null,
|
|
272
|
+
latestVersion,
|
|
273
|
+
};
|
|
274
|
+
} catch (error) {
|
|
275
|
+
return {
|
|
276
|
+
ok: false,
|
|
277
|
+
error: error instanceof Error ? error.message : 'Could not parse npm version output.',
|
|
278
|
+
latestVersion: null,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function buildUpdateStatus(home, statePatch = {}) {
|
|
284
|
+
const state = { ...readUpdateState(home), ...statePatch };
|
|
285
|
+
const installMode = detectInstallMode(repoRoot);
|
|
286
|
+
const npmBinary = resolveNpmBinary();
|
|
287
|
+
const launcherPath = resolveLauncherPath();
|
|
288
|
+
const support = updateSupportSummary(installMode, npmBinary, launcherPath);
|
|
289
|
+
const currentVersion = normalizeVersion(state.current_version || packageJson.version);
|
|
290
|
+
const latestVersion = normalizeVersion(state.latest_version || '');
|
|
291
|
+
const updateAvailable = Boolean(latestVersion) && compareVersions(latestVersion, currentVersion) > 0;
|
|
292
|
+
const skippedVersion = normalizeVersion(state.last_skipped_version || '');
|
|
293
|
+
const skippedCurrentTarget = Boolean(updateAvailable && skippedVersion && skippedVersion === latestVersion);
|
|
294
|
+
const promptRecommended =
|
|
295
|
+
Boolean(updateAvailable)
|
|
296
|
+
&& !Boolean(state.busy)
|
|
297
|
+
&& !skippedCurrentTarget
|
|
298
|
+
&& isExpired(state.last_prompted_at || state.last_deferred_at, UPDATE_PROMPT_TTL_MS);
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
ok: true,
|
|
302
|
+
package_name: UPDATE_PACKAGE_NAME,
|
|
303
|
+
install_mode: installMode,
|
|
304
|
+
can_check: support.canCheck,
|
|
305
|
+
can_self_update: support.canSelfUpdate,
|
|
306
|
+
current_version: currentVersion,
|
|
307
|
+
latest_version: latestVersion || null,
|
|
308
|
+
update_available: updateAvailable,
|
|
309
|
+
prompt_recommended: promptRecommended,
|
|
310
|
+
busy: Boolean(state.busy),
|
|
311
|
+
last_checked_at: state.last_checked_at || null,
|
|
312
|
+
last_check_error: state.last_check_error || null,
|
|
313
|
+
last_prompted_at: state.last_prompted_at || null,
|
|
314
|
+
last_deferred_at: state.last_deferred_at || null,
|
|
315
|
+
last_skipped_version: skippedVersion || null,
|
|
316
|
+
last_update_started_at: state.last_update_started_at || null,
|
|
317
|
+
last_update_finished_at: state.last_update_finished_at || null,
|
|
318
|
+
last_update_result: state.last_update_result || null,
|
|
319
|
+
target_version: normalizeVersion(state.target_version || '') || null,
|
|
320
|
+
manual_update_command: updateManualCommand(installMode),
|
|
321
|
+
reason: support.reason,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function checkForUpdates(home, { force = false, timeoutMs = 3500 } = {}) {
|
|
326
|
+
const currentVersion = normalizeVersion(packageJson.version);
|
|
327
|
+
const existing = readUpdateState(home);
|
|
328
|
+
const installMode = detectInstallMode(repoRoot);
|
|
329
|
+
const npmBinary = resolveNpmBinary();
|
|
330
|
+
const launcherPath = resolveLauncherPath();
|
|
331
|
+
const support = updateSupportSummary(installMode, npmBinary, launcherPath);
|
|
332
|
+
|
|
333
|
+
if (!force && existing.current_version === currentVersion && !isExpired(existing.last_checked_at, UPDATE_CHECK_TTL_MS)) {
|
|
334
|
+
return buildUpdateStatus(home);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!support.canCheck) {
|
|
338
|
+
const patched = mergeUpdateState(home, {
|
|
339
|
+
current_version: currentVersion,
|
|
340
|
+
last_checked_at: new Date().toISOString(),
|
|
341
|
+
last_check_error: support.reason,
|
|
342
|
+
});
|
|
343
|
+
return buildUpdateStatus(home, patched);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const probe = fetchLatestPublishedVersion({ npmBinary, timeoutMs });
|
|
347
|
+
const patched = mergeUpdateState(home, {
|
|
348
|
+
current_version: currentVersion,
|
|
349
|
+
latest_version: probe.latestVersion || existing.latest_version || null,
|
|
350
|
+
last_checked_at: new Date().toISOString(),
|
|
351
|
+
last_check_error: probe.ok ? null : probe.error,
|
|
352
|
+
});
|
|
353
|
+
return buildUpdateStatus(home, patched);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function markUpdateDeferred(home, version) {
|
|
357
|
+
const patched = mergeUpdateState(home, {
|
|
358
|
+
last_prompted_at: new Date().toISOString(),
|
|
359
|
+
last_deferred_at: new Date().toISOString(),
|
|
360
|
+
latest_version: normalizeVersion(version || readUpdateState(home).latest_version || '') || null,
|
|
361
|
+
});
|
|
362
|
+
return buildUpdateStatus(home, patched);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function markUpdateSkipped(home, version) {
|
|
366
|
+
const normalized = normalizeVersion(version);
|
|
367
|
+
const patched = mergeUpdateState(home, {
|
|
368
|
+
last_prompted_at: new Date().toISOString(),
|
|
369
|
+
last_skipped_version: normalized || null,
|
|
370
|
+
});
|
|
371
|
+
return buildUpdateStatus(home, patched);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function parseRequiredPythonSpec(pyprojectText) {
|
|
375
|
+
const match = String(pyprojectText || '').match(/^\s*requires-python\s*=\s*["']([^"']+)["']/m);
|
|
376
|
+
return match ? match[1].trim() : '>=3.11';
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function parseMinimumPythonVersion(spec) {
|
|
380
|
+
const match = String(spec || '').match(/>=\s*(\d+)\.(\d+)(?:\.(\d+))?/);
|
|
381
|
+
if (!match) {
|
|
382
|
+
return { major: 3, minor: 11, patch: 0 };
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
major: Number(match[1]),
|
|
386
|
+
minor: Number(match[2]),
|
|
387
|
+
patch: Number(match[3] || 0),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
61
391
|
function resolveHome(args) {
|
|
62
392
|
const index = args.indexOf('--home');
|
|
63
393
|
if (index >= 0 && index + 1 < args.length) {
|
|
64
394
|
return path.resolve(args[index + 1]);
|
|
65
395
|
}
|
|
396
|
+
if (args.includes('--hero') || args.includes('--here')) {
|
|
397
|
+
return process.cwd();
|
|
398
|
+
}
|
|
66
399
|
if (process.env.DEEPSCIENTIST_HOME) {
|
|
67
400
|
return path.resolve(process.env.DEEPSCIENTIST_HOME);
|
|
68
401
|
}
|
|
402
|
+
if (process.env.DS_HOME) {
|
|
403
|
+
return path.resolve(process.env.DS_HOME);
|
|
404
|
+
}
|
|
69
405
|
return path.join(os.homedir(), 'DeepScientist');
|
|
70
406
|
}
|
|
71
407
|
|
|
@@ -176,11 +512,88 @@ function renderBrandArtwork() {
|
|
|
176
512
|
return [];
|
|
177
513
|
}
|
|
178
514
|
|
|
179
|
-
function
|
|
515
|
+
function truncateMiddle(text, maxLength = 120) {
|
|
516
|
+
const value = String(text || '');
|
|
517
|
+
if (value.length <= maxLength) {
|
|
518
|
+
return value;
|
|
519
|
+
}
|
|
520
|
+
const head = Math.max(24, Math.floor((maxLength - 1) / 2));
|
|
521
|
+
const tail = Math.max(16, maxLength - head - 1);
|
|
522
|
+
return `${value.slice(0, head)}…${value.slice(-tail)}`;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function renderKeyValueRows(rows) {
|
|
526
|
+
const labelWidth = Math.max(...rows.map(([label]) => String(label).length), 8);
|
|
527
|
+
for (const [label, value] of rows) {
|
|
528
|
+
console.log(` ${String(label).padEnd(labelWidth)} ${value}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function pythonMajorMinor(probe) {
|
|
533
|
+
if (!probe || typeof probe.major !== 'number' || typeof probe.minor !== 'number') {
|
|
534
|
+
return '';
|
|
535
|
+
}
|
|
536
|
+
return `${probe.major}.${probe.minor}`;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function pythonVersionText(probe) {
|
|
540
|
+
if (!probe) {
|
|
541
|
+
return 'unknown';
|
|
542
|
+
}
|
|
543
|
+
const version = probe.version || pythonMajorMinor(probe) || 'unknown';
|
|
544
|
+
if (probe.executable) {
|
|
545
|
+
return `${version} (${probe.executable})`;
|
|
546
|
+
}
|
|
547
|
+
return version;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function renderLaunchHints({ home, url, bindUrl, pythonSelection }) {
|
|
551
|
+
const runtimeRows = [
|
|
552
|
+
['Version', packageJson.version],
|
|
553
|
+
['Home', truncateMiddle(home)],
|
|
554
|
+
['Browser URL', url],
|
|
555
|
+
['Bind URL', bindUrl],
|
|
556
|
+
['Python', truncateMiddle(pythonVersionText(pythonSelection))],
|
|
557
|
+
];
|
|
558
|
+
if (pythonSelection && pythonSelection.sourceLabel) {
|
|
559
|
+
runtimeRows.push(['Python source', pythonSelection.sourceLabel]);
|
|
560
|
+
}
|
|
561
|
+
console.log(colorize('\u001B[1;38;5;39m', 'Runtime'));
|
|
562
|
+
renderKeyValueRows(runtimeRows);
|
|
563
|
+
console.log('');
|
|
564
|
+
|
|
565
|
+
console.log(colorize('\u001B[1;38;5;39m', 'Quick Flags'));
|
|
566
|
+
renderKeyValueRows([
|
|
567
|
+
['ds --port 21000', 'Change the web port'],
|
|
568
|
+
['ds --host 0.0.0.0 --port 21000', 'Bind on all interfaces'],
|
|
569
|
+
['ds --hero', 'Use the current directory as home'],
|
|
570
|
+
['ds --both', 'Start web + TUI together'],
|
|
571
|
+
['ds --tui', 'Start the terminal workspace only'],
|
|
572
|
+
['ds --no-browser', 'Do not auto-open the browser'],
|
|
573
|
+
['ds --status', 'Show daemon health as JSON'],
|
|
574
|
+
['ds --restart', 'Restart the managed daemon'],
|
|
575
|
+
['ds --stop', 'Stop the managed daemon'],
|
|
576
|
+
['ds migrate /data/DeepScientist', 'Move the full home/install root safely'],
|
|
577
|
+
['ds --help', 'Show the full launcher help'],
|
|
578
|
+
]);
|
|
579
|
+
console.log('');
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function printLaunchCard({
|
|
583
|
+
url,
|
|
584
|
+
bindUrl,
|
|
585
|
+
mode,
|
|
586
|
+
autoOpenRequested,
|
|
587
|
+
browserOpened,
|
|
588
|
+
daemonOnly,
|
|
589
|
+
home,
|
|
590
|
+
pythonSelection,
|
|
591
|
+
}) {
|
|
180
592
|
const width = Math.max(72, Math.min(process.stdout.columns || 100, 108));
|
|
181
593
|
const divider = colorize('\u001B[38;5;245m', '─'.repeat(Math.max(36, width - 6)));
|
|
182
594
|
const title = colorize('\u001B[1;38;5;39m', 'ResearAI');
|
|
183
595
|
const subtitle = colorize('\u001B[38;5;110m', 'Local-first research operating system');
|
|
596
|
+
const versionLine = colorize('\u001B[38;5;245m', `Version ${packageJson.version}`);
|
|
184
597
|
const urlLabel = colorize('\u001B[1;38;5;45m', hyperlink(url, url));
|
|
185
598
|
const workspaceMode =
|
|
186
599
|
mode === 'both'
|
|
@@ -218,6 +631,7 @@ function printLaunchCard({ url, bindUrl, mode, autoOpenRequested, browserOpened,
|
|
|
218
631
|
' |_| ',
|
|
219
632
|
];
|
|
220
633
|
console.log(centerText(title, width));
|
|
634
|
+
console.log(centerText(versionLine, width));
|
|
221
635
|
for (const line of wordmark) {
|
|
222
636
|
console.log(centerText(colorize('\u001B[1;38;5;39m', line), width));
|
|
223
637
|
}
|
|
@@ -228,10 +642,11 @@ function printLaunchCard({ url, bindUrl, mode, autoOpenRequested, browserOpened,
|
|
|
228
642
|
console.log(centerText(urlLabel, width));
|
|
229
643
|
console.log(centerText(divider, width));
|
|
230
644
|
console.log(centerText(browserLine, width));
|
|
231
|
-
console.log(centerText(`Daemon bind: ${bindUrl}`, width));
|
|
232
645
|
console.log(centerText(nextStep, width));
|
|
233
646
|
console.log(centerText('Run ds --stop to stop the managed daemon.', width));
|
|
647
|
+
console.log(centerText('Need to move this installation later? Use ds migrate /new/path.', width));
|
|
234
648
|
console.log('');
|
|
649
|
+
renderLaunchHints({ home, url, bindUrl, pythonSelection });
|
|
235
650
|
}
|
|
236
651
|
|
|
237
652
|
function escapeHtml(value) {
|
|
@@ -354,7 +769,7 @@ function writeCodexPreflightReport(home, probe) {
|
|
|
354
769
|
};
|
|
355
770
|
}
|
|
356
771
|
|
|
357
|
-
function readCodexBootstrapState(home,
|
|
772
|
+
function readCodexBootstrapState(home, runtimePython) {
|
|
358
773
|
const snippet = [
|
|
359
774
|
'import json, pathlib, sys',
|
|
360
775
|
'from deepscientist.config import ConfigManager',
|
|
@@ -362,7 +777,7 @@ function readCodexBootstrapState(home, venvPython) {
|
|
|
362
777
|
'manager = ConfigManager(home)',
|
|
363
778
|
'print(json.dumps(manager.codex_bootstrap_state(), ensure_ascii=False))',
|
|
364
779
|
].join('\n');
|
|
365
|
-
const result = runSync(
|
|
780
|
+
const result = runSync(runtimePython, ['-c', snippet, home], { capture: true, allowFailure: true });
|
|
366
781
|
if (result.status !== 0) {
|
|
367
782
|
return { codex_ready: false, codex_last_checked_at: null, codex_last_result: {} };
|
|
368
783
|
}
|
|
@@ -373,7 +788,7 @@ function readCodexBootstrapState(home, venvPython) {
|
|
|
373
788
|
}
|
|
374
789
|
}
|
|
375
790
|
|
|
376
|
-
function probeCodexBootstrap(home,
|
|
791
|
+
function probeCodexBootstrap(home, runtimePython) {
|
|
377
792
|
const snippet = [
|
|
378
793
|
'import json, pathlib, sys',
|
|
379
794
|
'from deepscientist.config import ConfigManager',
|
|
@@ -381,7 +796,7 @@ function probeCodexBootstrap(home, venvPython) {
|
|
|
381
796
|
'manager = ConfigManager(home)',
|
|
382
797
|
'print(json.dumps(manager.probe_codex_bootstrap(persist=True), ensure_ascii=False))',
|
|
383
798
|
].join('\n');
|
|
384
|
-
const result = runSync(
|
|
799
|
+
const result = runSync(runtimePython, ['-c', snippet, home], { capture: true, allowFailure: true });
|
|
385
800
|
let payload = null;
|
|
386
801
|
try {
|
|
387
802
|
payload = JSON.parse(result.stdout || '{}');
|
|
@@ -430,6 +845,7 @@ function parseLauncherArgs(argv) {
|
|
|
430
845
|
let questId = null;
|
|
431
846
|
let status = false;
|
|
432
847
|
let daemonOnly = false;
|
|
848
|
+
let skipUpdateCheck = false;
|
|
433
849
|
|
|
434
850
|
if (args[0] === 'ui') {
|
|
435
851
|
args.shift();
|
|
@@ -447,6 +863,7 @@ function parseLauncherArgs(argv) {
|
|
|
447
863
|
else if (arg === '--no-browser') openBrowser = false;
|
|
448
864
|
else if (arg === '--open-browser') openBrowser = true;
|
|
449
865
|
else if (arg === '--daemon-only') daemonOnly = true;
|
|
866
|
+
else if (arg === '--skip-update-check') skipUpdateCheck = true;
|
|
450
867
|
else if (arg === '--host' && args[index + 1]) host = args[++index];
|
|
451
868
|
else if (arg === '--port' && args[index + 1]) port = Number(args[++index]);
|
|
452
869
|
else if (arg === '--home' && args[index + 1]) home = path.resolve(args[++index]);
|
|
@@ -468,6 +885,133 @@ function parseLauncherArgs(argv) {
|
|
|
468
885
|
openBrowser,
|
|
469
886
|
questId,
|
|
470
887
|
daemonOnly,
|
|
888
|
+
skipUpdateCheck,
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function printUpdateHelp() {
|
|
893
|
+
console.log(`DeepScientist update
|
|
894
|
+
|
|
895
|
+
Usage:
|
|
896
|
+
ds update
|
|
897
|
+
ds update --check
|
|
898
|
+
ds update --yes
|
|
899
|
+
ds update --remind-later
|
|
900
|
+
ds update --skip-version
|
|
901
|
+
|
|
902
|
+
Flags:
|
|
903
|
+
--check Return the current update status without installing
|
|
904
|
+
--yes Install the latest published npm package immediately
|
|
905
|
+
--json Print structured JSON output
|
|
906
|
+
--force-check Ignore the cached version probe
|
|
907
|
+
--remind-later Defer prompts for the current published version
|
|
908
|
+
--skip-version Skip reminders for the current published version
|
|
909
|
+
`);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function printMigrateHelp() {
|
|
913
|
+
console.log(`DeepScientist migrate
|
|
914
|
+
|
|
915
|
+
Usage:
|
|
916
|
+
ds migrate /absolute/target/path
|
|
917
|
+
ds migrate /absolute/target/path --yes
|
|
918
|
+
ds migrate /absolute/target/path --restart
|
|
919
|
+
ds migrate /absolute/target/path --home /current/source/path
|
|
920
|
+
|
|
921
|
+
Flags:
|
|
922
|
+
--yes Skip the interactive double-confirmation prompt
|
|
923
|
+
--restart Start the managed daemon again from the migrated home
|
|
924
|
+
--home <path> Override the current DeepScientist source home/root
|
|
925
|
+
`);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function parseUpdateArgs(argv) {
|
|
929
|
+
const args = [...argv];
|
|
930
|
+
if (args[0] === 'update') {
|
|
931
|
+
args.shift();
|
|
932
|
+
}
|
|
933
|
+
let json = false;
|
|
934
|
+
let check = false;
|
|
935
|
+
let yes = false;
|
|
936
|
+
let forceCheck = false;
|
|
937
|
+
let remindLater = false;
|
|
938
|
+
let skipVersion = false;
|
|
939
|
+
let background = false;
|
|
940
|
+
let worker = false;
|
|
941
|
+
let home = null;
|
|
942
|
+
let host = null;
|
|
943
|
+
let port = null;
|
|
944
|
+
let restartDaemon = null;
|
|
945
|
+
let skipUpdateCheck = false;
|
|
946
|
+
|
|
947
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
948
|
+
const arg = args[index];
|
|
949
|
+
if (arg === '--json') json = true;
|
|
950
|
+
else if (arg === '--check') check = true;
|
|
951
|
+
else if (arg === '--yes') yes = true;
|
|
952
|
+
else if (arg === '--force-check') forceCheck = true;
|
|
953
|
+
else if (arg === '--remind-later') remindLater = true;
|
|
954
|
+
else if (arg === '--skip-version') skipVersion = true;
|
|
955
|
+
else if (arg === '--background') background = true;
|
|
956
|
+
else if (arg === '--worker') worker = true;
|
|
957
|
+
else if (arg === '--restart-daemon') restartDaemon = true;
|
|
958
|
+
else if (arg === '--skip-update-check') skipUpdateCheck = true;
|
|
959
|
+
else if (arg === '--home' && args[index + 1]) home = path.resolve(args[++index]);
|
|
960
|
+
else if (arg === '--host' && args[index + 1]) host = args[++index];
|
|
961
|
+
else if (arg === '--port' && args[index + 1]) port = Number(args[++index]);
|
|
962
|
+
else if (arg === '--help' || arg === '-h') return { help: true };
|
|
963
|
+
else if (!arg.startsWith('--')) return null;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
return {
|
|
967
|
+
help: false,
|
|
968
|
+
json,
|
|
969
|
+
check,
|
|
970
|
+
yes,
|
|
971
|
+
forceCheck,
|
|
972
|
+
remindLater,
|
|
973
|
+
skipVersion,
|
|
974
|
+
background,
|
|
975
|
+
worker,
|
|
976
|
+
home,
|
|
977
|
+
host,
|
|
978
|
+
port,
|
|
979
|
+
restartDaemon,
|
|
980
|
+
skipUpdateCheck,
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function parseMigrateArgs(argv) {
|
|
985
|
+
const args = [...argv];
|
|
986
|
+
if (args[0] === 'migrate') {
|
|
987
|
+
args.shift();
|
|
988
|
+
}
|
|
989
|
+
let home = null;
|
|
990
|
+
let target = null;
|
|
991
|
+
let yes = false;
|
|
992
|
+
let restart = false;
|
|
993
|
+
|
|
994
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
995
|
+
const arg = args[index];
|
|
996
|
+
if (arg === '--yes') yes = true;
|
|
997
|
+
else if (arg === '--restart') restart = true;
|
|
998
|
+
else if (arg === '--home' && args[index + 1]) home = path.resolve(expandUserPath(args[++index]));
|
|
999
|
+
else if (arg === '--help' || arg === '-h') return { help: true };
|
|
1000
|
+
else if (arg.startsWith('--')) return null;
|
|
1001
|
+
else if (!target) target = path.resolve(expandUserPath(arg));
|
|
1002
|
+
else return null;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
if (!target) {
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
return {
|
|
1010
|
+
help: false,
|
|
1011
|
+
home,
|
|
1012
|
+
target,
|
|
1013
|
+
yes,
|
|
1014
|
+
restart,
|
|
471
1015
|
};
|
|
472
1016
|
}
|
|
473
1017
|
|
|
@@ -486,76 +1030,498 @@ function findFirstPositionalArg(args) {
|
|
|
486
1030
|
return null;
|
|
487
1031
|
}
|
|
488
1032
|
|
|
489
|
-
function
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
}
|
|
1033
|
+
function realpathOrSelf(targetPath) {
|
|
1034
|
+
try {
|
|
1035
|
+
return fs.realpathSync(targetPath);
|
|
1036
|
+
} catch {
|
|
1037
|
+
return targetPath;
|
|
495
1038
|
}
|
|
496
|
-
console.error('DeepScientist could not find a working Python 3 interpreter.');
|
|
497
|
-
process.exit(1);
|
|
498
1039
|
}
|
|
499
1040
|
|
|
500
|
-
function
|
|
501
|
-
return
|
|
502
|
-
? path.join(home, 'runtime', 'venv', 'Scripts', 'python.exe')
|
|
503
|
-
: path.join(home, 'runtime', 'venv', 'bin', 'python');
|
|
1041
|
+
function isPathEqual(left, right) {
|
|
1042
|
+
return realpathOrSelf(path.resolve(left)) === realpathOrSelf(path.resolve(right));
|
|
504
1043
|
}
|
|
505
1044
|
|
|
506
|
-
function
|
|
507
|
-
|
|
1045
|
+
function isPathInside(candidatePath, parentPath) {
|
|
1046
|
+
const candidate = realpathOrSelf(path.resolve(candidatePath));
|
|
1047
|
+
const parent = realpathOrSelf(path.resolve(parentPath));
|
|
1048
|
+
if (candidate === parent) {
|
|
1049
|
+
return false;
|
|
1050
|
+
}
|
|
1051
|
+
const relative = path.relative(parent, candidate);
|
|
1052
|
+
return Boolean(relative && !relative.startsWith('..') && !path.isAbsolute(relative));
|
|
508
1053
|
}
|
|
509
1054
|
|
|
510
|
-
function
|
|
511
|
-
return
|
|
1055
|
+
function buildInstalledWrapperScript() {
|
|
1056
|
+
return [
|
|
1057
|
+
'#!/usr/bin/env bash',
|
|
1058
|
+
'set -euo pipefail',
|
|
1059
|
+
'SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"',
|
|
1060
|
+
'HOME_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"',
|
|
1061
|
+
'if [ -z "${DEEPSCIENTIST_HOME:-}" ]; then',
|
|
1062
|
+
' export DEEPSCIENTIST_HOME="$HOME_DIR"',
|
|
1063
|
+
'fi',
|
|
1064
|
+
'NODE_BIN="${DEEPSCIENTIST_NODE:-node}"',
|
|
1065
|
+
'exec "$NODE_BIN" "$SCRIPT_DIR/ds.js" "$@"',
|
|
1066
|
+
'',
|
|
1067
|
+
].join('\n');
|
|
512
1068
|
}
|
|
513
1069
|
|
|
514
|
-
function
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
1070
|
+
function buildGlobalWrapperScript({ installDir, home, commandName }) {
|
|
1071
|
+
return [
|
|
1072
|
+
'#!/usr/bin/env bash',
|
|
1073
|
+
'set -euo pipefail',
|
|
1074
|
+
'if [ -z "${DEEPSCIENTIST_HOME:-}" ]; then',
|
|
1075
|
+
` export DEEPSCIENTIST_HOME="${home}"`,
|
|
1076
|
+
'fi',
|
|
1077
|
+
`exec "${path.join(installDir, 'bin', commandName)}" "$@"`,
|
|
1078
|
+
'',
|
|
1079
|
+
].join('\n');
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function writeExecutableScript(targetPath, content) {
|
|
1083
|
+
ensureDir(path.dirname(targetPath));
|
|
1084
|
+
fs.writeFileSync(targetPath, content, { encoding: 'utf8', mode: 0o755 });
|
|
1085
|
+
fs.chmodSync(targetPath, 0o755);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function repairMigratedInstallWrappers(targetHome) {
|
|
1089
|
+
const installBinDir = path.join(targetHome, 'cli', 'bin');
|
|
1090
|
+
if (!fs.existsSync(installBinDir)) {
|
|
1091
|
+
return;
|
|
520
1092
|
}
|
|
521
|
-
const
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const fullPath = path.join(current, entry.name);
|
|
527
|
-
if (entry.isDirectory()) {
|
|
528
|
-
stack.push(fullPath);
|
|
529
|
-
continue;
|
|
530
|
-
}
|
|
531
|
-
if (entry.isFile()) {
|
|
532
|
-
files.push(fullPath);
|
|
533
|
-
}
|
|
1093
|
+
const content = buildInstalledWrapperScript();
|
|
1094
|
+
for (const commandName of launcherWrapperCommands) {
|
|
1095
|
+
const wrapperPath = path.join(installBinDir, commandName);
|
|
1096
|
+
if (!fs.existsSync(wrapperPath)) {
|
|
1097
|
+
continue;
|
|
534
1098
|
}
|
|
1099
|
+
writeExecutableScript(wrapperPath, content);
|
|
535
1100
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function candidateWrapperPathsForCommand(commandName) {
|
|
1104
|
+
const directories = String(process.env.PATH || '')
|
|
1105
|
+
.split(path.delimiter)
|
|
1106
|
+
.filter(Boolean);
|
|
1107
|
+
const candidates = [];
|
|
1108
|
+
for (const directory of directories) {
|
|
1109
|
+
candidates.push(path.join(directory, commandName));
|
|
1110
|
+
if (process.platform === 'win32') {
|
|
1111
|
+
candidates.push(path.join(directory, `${commandName}.cmd`));
|
|
1112
|
+
candidates.push(path.join(directory, `${commandName}.ps1`));
|
|
1113
|
+
}
|
|
540
1114
|
}
|
|
541
|
-
return
|
|
1115
|
+
return candidates;
|
|
542
1116
|
}
|
|
543
1117
|
|
|
544
|
-
function
|
|
545
|
-
|
|
546
|
-
if (!fs.existsSync(skillsRoot)) {
|
|
1118
|
+
function rewriteLauncherWrappersIfPointingAtSource({ sourceHome, targetHome }) {
|
|
1119
|
+
if (process.platform === 'win32') {
|
|
547
1120
|
return [];
|
|
548
1121
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
1122
|
+
const rewritten = [];
|
|
1123
|
+
const sourceInstallDir = path.join(sourceHome, 'cli');
|
|
1124
|
+
const targetInstallDir = path.join(targetHome, 'cli');
|
|
1125
|
+
for (const commandName of launcherWrapperCommands) {
|
|
1126
|
+
for (const candidate of candidateWrapperPathsForCommand(commandName)) {
|
|
1127
|
+
if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile()) {
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
let text = '';
|
|
1131
|
+
try {
|
|
1132
|
+
text = fs.readFileSync(candidate, 'utf8');
|
|
1133
|
+
} catch {
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
if (!text.includes(sourceInstallDir) && !text.includes(sourceHome)) {
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
writeExecutableScript(
|
|
1140
|
+
candidate,
|
|
1141
|
+
buildGlobalWrapperScript({
|
|
1142
|
+
installDir: targetInstallDir,
|
|
1143
|
+
home: targetHome,
|
|
1144
|
+
commandName,
|
|
1145
|
+
})
|
|
1146
|
+
);
|
|
1147
|
+
rewritten.push(candidate);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
return rewritten;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function scheduleDeferredSourceCleanup({ sourceHome, targetHome }) {
|
|
1154
|
+
const logPath = path.join(targetHome, 'logs', 'migrate-cleanup.log');
|
|
1155
|
+
ensureDir(path.dirname(logPath));
|
|
1156
|
+
const helperScript = [
|
|
1157
|
+
"const fs = require('node:fs');",
|
|
1158
|
+
"const { setTimeout: sleep } = require('node:timers/promises');",
|
|
1159
|
+
'const parentPid = Number(process.argv[1]);',
|
|
1160
|
+
'const sourceHome = process.argv[2];',
|
|
1161
|
+
'const logPath = process.argv[3];',
|
|
1162
|
+
'(async () => {',
|
|
1163
|
+
' for (let attempt = 0; attempt < 300; attempt += 1) {',
|
|
1164
|
+
' try {',
|
|
1165
|
+
' process.kill(parentPid, 0);',
|
|
1166
|
+
' await sleep(100);',
|
|
1167
|
+
' continue;',
|
|
1168
|
+
' } catch {',
|
|
1169
|
+
' break;',
|
|
1170
|
+
' }',
|
|
1171
|
+
' }',
|
|
1172
|
+
' try {',
|
|
1173
|
+
' fs.rmSync(sourceHome, { recursive: true, force: true });',
|
|
1174
|
+
' } catch (error) {',
|
|
1175
|
+
" fs.appendFileSync(logPath, `[${new Date().toISOString()}] ${error instanceof Error ? error.message : String(error)}\\n`, 'utf8');",
|
|
1176
|
+
' process.exit(1);',
|
|
1177
|
+
' }',
|
|
1178
|
+
'})();',
|
|
1179
|
+
].join('\n');
|
|
1180
|
+
const child = spawn(process.execPath, ['-e', helperScript, String(process.pid), sourceHome, logPath], {
|
|
1181
|
+
detached: true,
|
|
1182
|
+
stdio: 'ignore',
|
|
1183
|
+
env: process.env,
|
|
1184
|
+
});
|
|
1185
|
+
child.unref();
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
async function promptMigrationConfirmation({ sourceHome, targetHome }) {
|
|
1189
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1190
|
+
throw new Error('DeepScientist migration needs a TTY for confirmation. Re-run with `--yes` to continue non-interactively.');
|
|
1191
|
+
}
|
|
1192
|
+
console.log('');
|
|
1193
|
+
console.log('DeepScientist home migration');
|
|
1194
|
+
console.log('');
|
|
1195
|
+
console.log(`From: ${sourceHome}`);
|
|
1196
|
+
console.log(`To: ${targetHome}`);
|
|
1197
|
+
console.log('');
|
|
1198
|
+
console.log('This will stop the managed daemon, copy the full DeepScientist root, verify the copy, update launcher wrappers, and delete the old path after success.');
|
|
1199
|
+
const ask = (question) => new Promise((resolve) => {
|
|
1200
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1201
|
+
rl.question(question, (answer) => {
|
|
1202
|
+
rl.close();
|
|
1203
|
+
resolve(String(answer || '').trim());
|
|
1204
|
+
});
|
|
1205
|
+
});
|
|
1206
|
+
const first = await ask('Type YES to continue: ');
|
|
1207
|
+
if (first !== 'YES') {
|
|
1208
|
+
return false;
|
|
1209
|
+
}
|
|
1210
|
+
const second = await ask('Type MIGRATE to confirm old-path deletion after a successful copy: ');
|
|
1211
|
+
return second === 'MIGRATE';
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function printMigrationSummary({ sourceHome, targetHome, restart }) {
|
|
1215
|
+
console.log('');
|
|
1216
|
+
console.log('DeepScientist migrate');
|
|
1217
|
+
console.log('');
|
|
1218
|
+
console.log(`Source: ${sourceHome}`);
|
|
1219
|
+
console.log(`Target: ${targetHome}`);
|
|
1220
|
+
console.log(`Restart: ${restart ? 'yes' : 'no'}`);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
function pythonMeetsMinimum(probe) {
|
|
1224
|
+
if (!probe || typeof probe.major !== 'number' || typeof probe.minor !== 'number') {
|
|
1225
|
+
return false;
|
|
1226
|
+
}
|
|
1227
|
+
if (probe.major !== minimumPythonVersion.major) {
|
|
1228
|
+
return probe.major > minimumPythonVersion.major;
|
|
1229
|
+
}
|
|
1230
|
+
if (probe.minor !== minimumPythonVersion.minor) {
|
|
1231
|
+
return probe.minor > minimumPythonVersion.minor;
|
|
1232
|
+
}
|
|
1233
|
+
return probe.patch >= minimumPythonVersion.patch;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function pythonSelectionLabel(source) {
|
|
1237
|
+
if (source === 'conda') {
|
|
1238
|
+
const envName = String(process.env.CONDA_DEFAULT_ENV || '').trim();
|
|
1239
|
+
return envName ? `conda:${envName}` : 'conda';
|
|
1240
|
+
}
|
|
1241
|
+
if (source === 'uv-managed') {
|
|
1242
|
+
return 'uv-managed';
|
|
1243
|
+
}
|
|
1244
|
+
return 'path';
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function buildCondaPythonCandidates() {
|
|
1248
|
+
const prefix = String(process.env.CONDA_PREFIX || '').trim();
|
|
1249
|
+
if (!prefix) {
|
|
1250
|
+
return [];
|
|
1251
|
+
}
|
|
1252
|
+
if (process.platform === 'win32') {
|
|
1253
|
+
return [path.join(prefix, 'python.exe'), path.join(prefix, 'Scripts', 'python.exe')];
|
|
1254
|
+
}
|
|
1255
|
+
return [path.join(prefix, 'bin', 'python'), path.join(prefix, 'bin', 'python3')];
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function probePython(binary) {
|
|
1259
|
+
const snippet = [
|
|
1260
|
+
'import json, sys',
|
|
1261
|
+
'print(json.dumps({',
|
|
1262
|
+
' "executable": sys.executable,',
|
|
1263
|
+
' "version": ".".join(str(part) for part in sys.version_info[:3]),',
|
|
1264
|
+
' "major": sys.version_info[0],',
|
|
1265
|
+
' "minor": sys.version_info[1],',
|
|
1266
|
+
' "patch": sys.version_info[2],',
|
|
1267
|
+
'}, ensure_ascii=False))',
|
|
1268
|
+
].join('\n');
|
|
1269
|
+
const result = spawnSync(binary, ['-c', snippet], {
|
|
1270
|
+
encoding: 'utf8',
|
|
1271
|
+
env: process.env,
|
|
1272
|
+
});
|
|
1273
|
+
if (result.error) {
|
|
1274
|
+
return {
|
|
1275
|
+
ok: false,
|
|
1276
|
+
binary,
|
|
1277
|
+
error: result.error.message,
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
if (result.status !== 0) {
|
|
1281
|
+
return {
|
|
1282
|
+
ok: false,
|
|
1283
|
+
binary,
|
|
1284
|
+
error: (result.stderr || result.stdout || '').trim() || `exit ${result.status}`,
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
try {
|
|
1288
|
+
const payload = JSON.parse(result.stdout || '{}');
|
|
1289
|
+
const executable = String(payload.executable || '').trim();
|
|
1290
|
+
return {
|
|
1291
|
+
ok: true,
|
|
1292
|
+
binary,
|
|
1293
|
+
executable,
|
|
1294
|
+
realExecutable: executable ? realpathOrSelf(executable) : '',
|
|
1295
|
+
version: String(payload.version || '').trim(),
|
|
1296
|
+
major: Number(payload.major),
|
|
1297
|
+
minor: Number(payload.minor),
|
|
1298
|
+
patch: Number(payload.patch),
|
|
1299
|
+
};
|
|
1300
|
+
} catch (error) {
|
|
1301
|
+
return {
|
|
1302
|
+
ok: false,
|
|
1303
|
+
binary,
|
|
1304
|
+
error: error instanceof Error ? error.message : 'Could not parse Python version probe.',
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
function minimumPythonRequest() {
|
|
1310
|
+
return `${minimumPythonVersion.major}.${minimumPythonVersion.minor}`;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function decoratePythonProbe(probe, source) {
|
|
1314
|
+
if (!probe || !probe.ok) {
|
|
1315
|
+
return null;
|
|
1316
|
+
}
|
|
1317
|
+
return {
|
|
1318
|
+
...probe,
|
|
1319
|
+
source,
|
|
1320
|
+
sourceLabel: pythonSelectionLabel(source),
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function collectPythonProbes(binaries, source, seenExecutables) {
|
|
1325
|
+
const probes = [];
|
|
1326
|
+
for (const candidate of binaries) {
|
|
1327
|
+
const resolved = decoratePythonProbe(probePython(candidate), source);
|
|
1328
|
+
if (!resolved) {
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
const executableKey = resolved.realExecutable || resolved.executable || resolved.binary;
|
|
1332
|
+
if (seenExecutables.has(executableKey)) {
|
|
1333
|
+
continue;
|
|
1334
|
+
}
|
|
1335
|
+
seenExecutables.add(executableKey);
|
|
1336
|
+
probes.push(resolved);
|
|
1337
|
+
}
|
|
1338
|
+
return probes;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
function createPythonRuntimePlan({ condaProbes = [], pathProbes = [], minimumVersionRequest = minimumPythonRequest() }) {
|
|
1342
|
+
const validConda = condaProbes.find((probe) => pythonMeetsMinimum(probe)) || null;
|
|
1343
|
+
if (validConda) {
|
|
1344
|
+
return {
|
|
1345
|
+
runtimeKind: 'system',
|
|
1346
|
+
selectedProbe: validConda,
|
|
1347
|
+
source: 'conda',
|
|
1348
|
+
sourceLabel: validConda.sourceLabel,
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
const firstConda = condaProbes[0] || null;
|
|
1352
|
+
if (firstConda) {
|
|
1353
|
+
return {
|
|
1354
|
+
runtimeKind: 'managed',
|
|
1355
|
+
selectedProbe: null,
|
|
1356
|
+
rejectedProbe: firstConda,
|
|
1357
|
+
source: 'conda',
|
|
1358
|
+
sourceLabel: pythonSelectionLabel('uv-managed'),
|
|
1359
|
+
minimumVersionRequest,
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
const validPath = pathProbes.find((probe) => pythonMeetsMinimum(probe)) || null;
|
|
1364
|
+
if (validPath) {
|
|
1365
|
+
return {
|
|
1366
|
+
runtimeKind: 'system',
|
|
1367
|
+
selectedProbe: validPath,
|
|
1368
|
+
source: 'path',
|
|
1369
|
+
sourceLabel: validPath.sourceLabel,
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
const firstPath = pathProbes[0] || null;
|
|
1373
|
+
if (firstPath) {
|
|
1374
|
+
return {
|
|
1375
|
+
runtimeKind: 'managed',
|
|
1376
|
+
selectedProbe: null,
|
|
1377
|
+
rejectedProbe: firstPath,
|
|
1378
|
+
source: 'path',
|
|
1379
|
+
sourceLabel: pythonSelectionLabel('uv-managed'),
|
|
1380
|
+
minimumVersionRequest,
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
return {
|
|
1385
|
+
runtimeKind: 'managed',
|
|
1386
|
+
selectedProbe: null,
|
|
1387
|
+
rejectedProbe: null,
|
|
1388
|
+
source: 'uv-managed',
|
|
1389
|
+
sourceLabel: pythonSelectionLabel('uv-managed'),
|
|
1390
|
+
minimumVersionRequest,
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
function printManagedPythonFallbackNotice({ rejectedProbe, source, minimumVersionRequest, installDir }) {
|
|
1395
|
+
if (!rejectedProbe) {
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
const envName = String(process.env.CONDA_DEFAULT_ENV || '').trim();
|
|
1399
|
+
const sourceLabel =
|
|
1400
|
+
source === 'conda'
|
|
1401
|
+
? (envName ? `active conda environment \`${envName}\`` : 'active conda environment')
|
|
1402
|
+
: 'detected system Python';
|
|
1403
|
+
console.warn('');
|
|
1404
|
+
console.warn(
|
|
1405
|
+
`DeepScientist found ${sourceLabel} at ${pythonVersionText(rejectedProbe)}, which does not satisfy Python ${requiredPythonSpec}.`
|
|
1406
|
+
);
|
|
1407
|
+
console.warn(
|
|
1408
|
+
`DeepScientist will provision a uv-managed Python ${minimumVersionRequest}+ runtime under ${installDir}.`
|
|
1409
|
+
);
|
|
1410
|
+
console.warn('');
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
function resolvePythonRuntimePlan() {
|
|
1414
|
+
const seenExecutables = new Set();
|
|
1415
|
+
const condaProbes = collectPythonProbes(buildCondaPythonCandidates(), 'conda', seenExecutables);
|
|
1416
|
+
const pathProbes = collectPythonProbes(pythonCandidates, 'path', seenExecutables);
|
|
1417
|
+
return createPythonRuntimePlan({ condaProbes, pathProbes, minimumVersionRequest: minimumPythonRequest() });
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function runtimePythonEnvPath(home) {
|
|
1421
|
+
return path.join(home, 'runtime', 'python-env');
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function runtimePythonPath(home) {
|
|
1425
|
+
return process.platform === 'win32'
|
|
1426
|
+
? path.join(runtimePythonEnvPath(home), 'Scripts', 'python.exe')
|
|
1427
|
+
: path.join(runtimePythonEnvPath(home), 'bin', 'python');
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
function runtimeUvCachePath(home) {
|
|
1431
|
+
return path.join(home, 'runtime', 'uv-cache');
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
function runtimeUvPythonInstallPath(home) {
|
|
1435
|
+
return path.join(home, 'runtime', 'python');
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
function runtimeToolsPath(home) {
|
|
1439
|
+
return path.join(home, 'runtime', 'tools');
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function runtimeUvRootPath(home) {
|
|
1443
|
+
return path.join(runtimeToolsPath(home), 'uv');
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function runtimeUvBinDir(home) {
|
|
1447
|
+
return path.join(runtimeUvRootPath(home), 'bin');
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
function runtimeUvBinaryPath(home) {
|
|
1451
|
+
return path.join(runtimeUvBinDir(home), process.platform === 'win32' ? 'uv.exe' : 'uv');
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
function legacyVenvRootPath(home) {
|
|
1455
|
+
return path.join(home, 'runtime', 'venv');
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
function useEditableProjectInstall() {
|
|
1459
|
+
return fs.existsSync(path.join(repoRoot, '.git'));
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
function uvLockPath() {
|
|
1463
|
+
return path.join(repoRoot, 'uv.lock');
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
function sha256File(filePath) {
|
|
1467
|
+
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
function hashDirectoryTree(rootPath, predicate = null) {
|
|
1471
|
+
const hasher = crypto.createHash('sha256');
|
|
1472
|
+
if (!fs.existsSync(rootPath)) {
|
|
1473
|
+
hasher.update('missing');
|
|
1474
|
+
return hasher.digest('hex');
|
|
1475
|
+
}
|
|
1476
|
+
const stack = [rootPath];
|
|
1477
|
+
const files = [];
|
|
1478
|
+
while (stack.length > 0) {
|
|
1479
|
+
const current = stack.pop();
|
|
1480
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
1481
|
+
const fullPath = path.join(current, entry.name);
|
|
1482
|
+
if (entry.isDirectory()) {
|
|
1483
|
+
stack.push(fullPath);
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
if (entry.isFile()) {
|
|
1487
|
+
if (typeof predicate === 'function' && !predicate(fullPath)) {
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
files.push(fullPath);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
files.sort();
|
|
1495
|
+
for (const filePath of files) {
|
|
1496
|
+
hasher.update(path.relative(rootPath, filePath));
|
|
1497
|
+
hasher.update(fs.readFileSync(filePath));
|
|
1498
|
+
}
|
|
1499
|
+
return hasher.digest('hex');
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function hashSkillTree() {
|
|
1503
|
+
return hashDirectoryTree(path.join(repoRoot, 'src', 'skills'));
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
function hashPythonSourceTree() {
|
|
1507
|
+
return hashDirectoryTree(path.join(repoRoot, 'src', 'deepscientist'), (filePath) => filePath.endsWith('.py'));
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
function discoverSkillIds() {
|
|
1511
|
+
const skillsRoot = path.join(repoRoot, 'src', 'skills');
|
|
1512
|
+
if (!fs.existsSync(skillsRoot)) {
|
|
1513
|
+
return [];
|
|
1514
|
+
}
|
|
1515
|
+
return fs
|
|
1516
|
+
.readdirSync(skillsRoot, { withFileTypes: true })
|
|
1517
|
+
.filter(
|
|
1518
|
+
(entry) =>
|
|
1519
|
+
entry.isDirectory() &&
|
|
1520
|
+
!entry.name.startsWith('.') &&
|
|
1521
|
+
fs.existsSync(path.join(skillsRoot, entry.name, 'SKILL.md'))
|
|
1522
|
+
)
|
|
1523
|
+
.map((entry) => entry.name)
|
|
1524
|
+
.sort();
|
|
559
1525
|
}
|
|
560
1526
|
|
|
561
1527
|
function globalSkillsInstalled() {
|
|
@@ -571,15 +1537,11 @@ function globalSkillsInstalled() {
|
|
|
571
1537
|
|
|
572
1538
|
function runSync(binary, args, options = {}) {
|
|
573
1539
|
const result = spawnSync(binary, args, {
|
|
574
|
-
cwd: repoRoot,
|
|
1540
|
+
cwd: options.cwd || repoRoot,
|
|
575
1541
|
stdio: options.capture ? 'pipe' : 'inherit',
|
|
576
|
-
env:
|
|
577
|
-
...process.env,
|
|
578
|
-
PYTHONPATH: process.env.PYTHONPATH
|
|
579
|
-
? `${srcPath}${path.delimiter}${process.env.PYTHONPATH}`
|
|
580
|
-
: srcPath,
|
|
581
|
-
},
|
|
1542
|
+
env: options.env || process.env,
|
|
582
1543
|
encoding: 'utf8',
|
|
1544
|
+
input: options.input,
|
|
583
1545
|
});
|
|
584
1546
|
if (result.error) {
|
|
585
1547
|
throw result.error;
|
|
@@ -597,72 +1559,468 @@ function step(index, total, message) {
|
|
|
597
1559
|
console.log(`[${index}/${total}] ${message}`);
|
|
598
1560
|
}
|
|
599
1561
|
|
|
600
|
-
function
|
|
601
|
-
runSync(venvPython, ['-m', 'pip', 'install', '--upgrade', 'pip', 'setuptools', 'wheel']);
|
|
602
|
-
runSync(venvPython, ['-m', 'pip', 'install', '--upgrade', repoRoot]);
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
function verifyPythonRuntime(venvPython) {
|
|
1562
|
+
function verifyPythonRuntime(runtimePython) {
|
|
606
1563
|
const result = runSync(
|
|
607
|
-
|
|
1564
|
+
runtimePython,
|
|
608
1565
|
['-c', 'import deepscientist.cli; import cryptography; import _cffi_backend; print("ok")'],
|
|
609
1566
|
{ capture: true, allowFailure: true }
|
|
610
1567
|
);
|
|
611
1568
|
return result.status === 0;
|
|
612
1569
|
}
|
|
613
1570
|
|
|
614
|
-
function
|
|
615
|
-
fs.
|
|
616
|
-
|
|
617
|
-
|
|
1571
|
+
function readJsonFile(filePath) {
|
|
1572
|
+
if (!fs.existsSync(filePath)) {
|
|
1573
|
+
return null;
|
|
1574
|
+
}
|
|
1575
|
+
try {
|
|
1576
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
1577
|
+
} catch {
|
|
1578
|
+
return null;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
function executableExtensions() {
|
|
1583
|
+
if (process.platform !== 'win32') {
|
|
1584
|
+
return [''];
|
|
1585
|
+
}
|
|
1586
|
+
return (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM')
|
|
1587
|
+
.split(';')
|
|
1588
|
+
.filter(Boolean);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
function candidateExecutablePaths(basePath) {
|
|
1592
|
+
if (process.platform !== 'win32') {
|
|
1593
|
+
return [basePath];
|
|
1594
|
+
}
|
|
1595
|
+
const extension = path.extname(basePath);
|
|
1596
|
+
if (extension) {
|
|
1597
|
+
return [basePath];
|
|
1598
|
+
}
|
|
1599
|
+
return executableExtensions().map((suffix) => `${basePath}${suffix}`);
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
function isExecutableFile(candidate) {
|
|
1603
|
+
try {
|
|
1604
|
+
if (!fs.existsSync(candidate)) {
|
|
1605
|
+
return false;
|
|
1606
|
+
}
|
|
1607
|
+
const stat = fs.statSync(candidate);
|
|
1608
|
+
if (!stat.isFile()) {
|
|
1609
|
+
return false;
|
|
1610
|
+
}
|
|
1611
|
+
if (process.platform !== 'win32') {
|
|
1612
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
1613
|
+
}
|
|
1614
|
+
return true;
|
|
1615
|
+
} catch {
|
|
1616
|
+
return false;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
function resolveBinaryReference(reference) {
|
|
1621
|
+
const normalized = String(reference || '').trim();
|
|
1622
|
+
if (!normalized) {
|
|
1623
|
+
return null;
|
|
1624
|
+
}
|
|
1625
|
+
const expanded = expandUserPath(normalized);
|
|
1626
|
+
if (
|
|
1627
|
+
path.isAbsolute(expanded)
|
|
1628
|
+
|| normalized.startsWith('.')
|
|
1629
|
+
|| normalized.includes(path.sep)
|
|
1630
|
+
|| (path.sep === '\\' ? normalized.includes('/') : normalized.includes('\\'))
|
|
1631
|
+
) {
|
|
1632
|
+
const absolute = path.isAbsolute(expanded) ? expanded : path.resolve(expanded);
|
|
1633
|
+
for (const candidate of candidateExecutablePaths(absolute)) {
|
|
1634
|
+
if (isExecutableFile(candidate)) {
|
|
1635
|
+
return candidate;
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
return null;
|
|
1639
|
+
}
|
|
1640
|
+
return resolveExecutableOnPath(expanded);
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
function resolveUvBinary(home) {
|
|
1644
|
+
const configured = String(process.env.DEEPSCIENTIST_UV || process.env.UV_BIN || '').trim();
|
|
1645
|
+
if (configured) {
|
|
1646
|
+
return {
|
|
1647
|
+
path: resolveBinaryReference(configured),
|
|
1648
|
+
source: 'env',
|
|
1649
|
+
configured,
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
const local = resolveBinaryReference(runtimeUvBinaryPath(home));
|
|
1653
|
+
if (local) {
|
|
1654
|
+
return {
|
|
1655
|
+
path: local,
|
|
1656
|
+
source: 'local',
|
|
1657
|
+
configured: null,
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
const discovered = resolveExecutableOnPath('uv');
|
|
1661
|
+
return {
|
|
1662
|
+
path: discovered,
|
|
1663
|
+
source: discovered ? 'path' : null,
|
|
1664
|
+
configured: null,
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
function printUvInstallGuidance(home, errorMessage = null) {
|
|
1669
|
+
console.error('');
|
|
1670
|
+
if (errorMessage) {
|
|
1671
|
+
console.error(`DeepScientist could not prepare a local uv runtime manager: ${errorMessage}`);
|
|
1672
|
+
} else {
|
|
1673
|
+
console.error('DeepScientist could not find a usable uv runtime manager.');
|
|
1674
|
+
}
|
|
1675
|
+
console.error(`DeepScientist normally installs uv automatically under ${runtimeUvBinDir(home)}.`);
|
|
1676
|
+
console.error('If the automatic bootstrap fails, install uv manually and run `ds` again.');
|
|
1677
|
+
console.error('');
|
|
1678
|
+
if (process.platform === 'win32') {
|
|
1679
|
+
console.error('Windows PowerShell:');
|
|
1680
|
+
console.error(' powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"');
|
|
1681
|
+
} else {
|
|
1682
|
+
console.error('macOS / Linux:');
|
|
1683
|
+
console.error(' curl -LsSf https://astral.sh/uv/install.sh | sh');
|
|
1684
|
+
}
|
|
1685
|
+
console.error('Alternative:');
|
|
1686
|
+
console.error(' pipx install uv');
|
|
1687
|
+
console.error('');
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
function downloadFileWithNode(url, destinationPath) {
|
|
1691
|
+
const downloader = [
|
|
1692
|
+
'const fs = require("node:fs");',
|
|
1693
|
+
'const url = process.argv[1];',
|
|
1694
|
+
'const destination = process.argv[2];',
|
|
1695
|
+
'const timeoutMs = Number(process.argv[3] || "45000");',
|
|
1696
|
+
'(async () => {',
|
|
1697
|
+
' const controller = new AbortController();',
|
|
1698
|
+
' const timer = setTimeout(() => controller.abort(), timeoutMs);',
|
|
1699
|
+
' try {',
|
|
1700
|
+
' const response = await fetch(url, { signal: controller.signal });',
|
|
1701
|
+
' if (!response.ok) {',
|
|
1702
|
+
' throw new Error(`HTTP ${response.status} ${response.statusText}`);',
|
|
1703
|
+
' }',
|
|
1704
|
+
' const body = await response.text();',
|
|
1705
|
+
' fs.writeFileSync(destination, body, "utf8");',
|
|
1706
|
+
' } finally {',
|
|
1707
|
+
' clearTimeout(timer);',
|
|
1708
|
+
' }',
|
|
1709
|
+
'})().catch((error) => {',
|
|
1710
|
+
' console.error(error instanceof Error ? error.message : String(error));',
|
|
1711
|
+
' process.exit(1);',
|
|
1712
|
+
'});',
|
|
1713
|
+
].join('\n');
|
|
1714
|
+
const result = spawnSync(process.execPath, ['-e', downloader, url, destinationPath, '45000'], {
|
|
1715
|
+
cwd: repoRoot,
|
|
1716
|
+
stdio: 'inherit',
|
|
1717
|
+
env: process.env,
|
|
1718
|
+
});
|
|
1719
|
+
if (result.error) {
|
|
1720
|
+
throw result.error;
|
|
1721
|
+
}
|
|
1722
|
+
if (result.status !== 0) {
|
|
1723
|
+
throw new Error(`Download failed with status ${result.status ?? 1}.`);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
function installLocalUv(home) {
|
|
1728
|
+
const uvRoot = runtimeUvRootPath(home);
|
|
1729
|
+
const binDir = runtimeUvBinDir(home);
|
|
1730
|
+
const tempDir = path.join(uvRoot, 'tmp');
|
|
1731
|
+
const installerName = process.platform === 'win32' ? 'install-uv.ps1' : 'install-uv.sh';
|
|
1732
|
+
const installerUrl =
|
|
1733
|
+
process.platform === 'win32'
|
|
1734
|
+
? 'https://astral.sh/uv/install.ps1'
|
|
1735
|
+
: 'https://astral.sh/uv/install.sh';
|
|
1736
|
+
const installerPath = path.join(tempDir, installerName);
|
|
1737
|
+
|
|
1738
|
+
ensureDir(binDir);
|
|
1739
|
+
ensureDir(tempDir);
|
|
1740
|
+
|
|
1741
|
+
console.log(`DeepScientist is installing a local uv runtime manager under ${binDir}.`);
|
|
1742
|
+
downloadFileWithNode(installerUrl, installerPath);
|
|
1743
|
+
|
|
1744
|
+
const installEnv = {
|
|
1745
|
+
...process.env,
|
|
1746
|
+
UV_UNMANAGED_INSTALL: binDir,
|
|
1747
|
+
};
|
|
1748
|
+
|
|
1749
|
+
let shellBinary;
|
|
1750
|
+
let shellArgs;
|
|
1751
|
+
if (process.platform === 'win32') {
|
|
1752
|
+
shellBinary =
|
|
1753
|
+
resolveExecutableOnPath('powershell.exe')
|
|
1754
|
+
|| resolveExecutableOnPath('powershell')
|
|
1755
|
+
|| resolveExecutableOnPath('pwsh.exe')
|
|
1756
|
+
|| resolveExecutableOnPath('pwsh');
|
|
1757
|
+
if (!shellBinary) {
|
|
1758
|
+
throw new Error('PowerShell is not available to run the official uv installer.');
|
|
1759
|
+
}
|
|
1760
|
+
shellArgs = ['-ExecutionPolicy', 'ByPass', '-File', installerPath];
|
|
1761
|
+
} else {
|
|
1762
|
+
shellBinary = resolveExecutableOnPath('sh');
|
|
1763
|
+
if (!shellBinary) {
|
|
1764
|
+
throw new Error('`sh` is not available to run the official uv installer.');
|
|
1765
|
+
}
|
|
1766
|
+
shellArgs = [installerPath];
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
const installResult = spawnSync(shellBinary, shellArgs, {
|
|
1770
|
+
cwd: repoRoot,
|
|
1771
|
+
stdio: 'inherit',
|
|
1772
|
+
env: installEnv,
|
|
1773
|
+
});
|
|
1774
|
+
if (installResult.error) {
|
|
1775
|
+
throw installResult.error;
|
|
1776
|
+
}
|
|
1777
|
+
if (installResult.status !== 0) {
|
|
1778
|
+
throw new Error(`The official uv installer exited with status ${installResult.status ?? 1}.`);
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
const installedBinary = resolveBinaryReference(runtimeUvBinaryPath(home));
|
|
1782
|
+
if (!installedBinary) {
|
|
1783
|
+
throw new Error(`uv installation finished, but no executable was found under ${binDir}.`);
|
|
1784
|
+
}
|
|
1785
|
+
return installedBinary;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
function ensureUvBinary(home) {
|
|
1789
|
+
const resolved = resolveUvBinary(home);
|
|
1790
|
+
if (resolved.path) {
|
|
1791
|
+
return resolved.path;
|
|
1792
|
+
}
|
|
1793
|
+
if (resolved.source === 'env' && resolved.configured) {
|
|
1794
|
+
throw new Error(`Configured uv binary could not be resolved: ${resolved.configured}`);
|
|
1795
|
+
}
|
|
1796
|
+
return installLocalUv(home);
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
function buildUvRuntimeEnv(home, extraEnv = {}) {
|
|
1800
|
+
return {
|
|
1801
|
+
...process.env,
|
|
1802
|
+
UV_CACHE_DIR: runtimeUvCachePath(home),
|
|
1803
|
+
UV_PROJECT_ENVIRONMENT: runtimePythonEnvPath(home),
|
|
1804
|
+
UV_PYTHON_INSTALL_DIR: runtimeUvPythonInstallPath(home),
|
|
1805
|
+
...extraEnv,
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
function ensureUvLockPresent() {
|
|
1810
|
+
const lockPath = uvLockPath();
|
|
1811
|
+
if (fs.existsSync(lockPath)) {
|
|
1812
|
+
return lockPath;
|
|
1813
|
+
}
|
|
1814
|
+
console.error('DeepScientist is missing `uv.lock` in the installed package.');
|
|
1815
|
+
console.error('Reinstall the npm package, or from a source checkout run `uv lock` and try again.');
|
|
1816
|
+
process.exit(1);
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
function resolveUvVersion(uvBinary) {
|
|
1820
|
+
const result = runSync(uvBinary, ['--version'], { capture: true, allowFailure: true });
|
|
1821
|
+
if (result.status !== 0) {
|
|
1822
|
+
return null;
|
|
1823
|
+
}
|
|
1824
|
+
return String(result.stdout || '').trim() || null;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
function ensureUvManagedPython(home, uvBinary, minimumVersionRequest) {
|
|
1828
|
+
ensureDir(runtimeUvPythonInstallPath(home));
|
|
1829
|
+
ensureDir(runtimeUvCachePath(home));
|
|
1830
|
+
step(1, 4, `Provisioning uv-managed Python ${minimumVersionRequest}+`);
|
|
1831
|
+
const installResult = runSync(
|
|
1832
|
+
uvBinary,
|
|
1833
|
+
['python', 'install', minimumVersionRequest],
|
|
1834
|
+
{
|
|
1835
|
+
allowFailure: true,
|
|
1836
|
+
env: buildUvRuntimeEnv(home),
|
|
1837
|
+
}
|
|
1838
|
+
);
|
|
1839
|
+
if (installResult.status !== 0) {
|
|
1840
|
+
console.error('DeepScientist could not install a uv-managed Python runtime.');
|
|
1841
|
+
process.exit(installResult.status ?? 1);
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
const findResult = runSync(
|
|
1845
|
+
uvBinary,
|
|
1846
|
+
['python', 'find', '--managed-python', minimumVersionRequest],
|
|
1847
|
+
{
|
|
1848
|
+
capture: true,
|
|
1849
|
+
allowFailure: true,
|
|
1850
|
+
env: buildUvRuntimeEnv(home),
|
|
1851
|
+
}
|
|
1852
|
+
);
|
|
1853
|
+
const managedPython = String(findResult.stdout || '')
|
|
1854
|
+
.trim()
|
|
1855
|
+
.split(/\r?\n/)
|
|
1856
|
+
.filter(Boolean)
|
|
1857
|
+
.pop();
|
|
1858
|
+
if (!managedPython) {
|
|
1859
|
+
console.error('DeepScientist installed uv-managed Python, but could not locate the interpreter afterward.');
|
|
1860
|
+
process.exit(findResult.status ?? 1);
|
|
1861
|
+
}
|
|
1862
|
+
const probe = decoratePythonProbe(probePython(managedPython), 'uv-managed');
|
|
1863
|
+
if (!probe || !pythonMeetsMinimum(probe)) {
|
|
1864
|
+
console.error('DeepScientist found a uv-managed Python, but it does not satisfy the required version.');
|
|
1865
|
+
process.exit(1);
|
|
1866
|
+
}
|
|
1867
|
+
return probe;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
function syncUvProjectEnvironment(home, uvBinary, pythonTarget, editable) {
|
|
1871
|
+
const args = ['sync', '--frozen', '--no-dev', '--compile-bytecode', '--python', pythonTarget];
|
|
1872
|
+
if (!editable) {
|
|
1873
|
+
args.push('--no-editable');
|
|
1874
|
+
}
|
|
1875
|
+
step(2, 4, 'Syncing locked Python environment');
|
|
1876
|
+
const result = runSync(uvBinary, args, {
|
|
1877
|
+
allowFailure: true,
|
|
1878
|
+
env: buildUvRuntimeEnv(home),
|
|
1879
|
+
});
|
|
1880
|
+
if (result.status === 0) {
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
console.error('DeepScientist could not sync the locked Python environment with uv.');
|
|
1884
|
+
console.error('If you are working from a source checkout, run `uv lock` after dependency changes and try again.');
|
|
1885
|
+
process.exit(result.status ?? 1);
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
function createRuntimeSelectionProbe(runtimeProbe, sourceLabel) {
|
|
1889
|
+
return {
|
|
1890
|
+
...runtimeProbe,
|
|
1891
|
+
sourceLabel,
|
|
1892
|
+
};
|
|
618
1893
|
}
|
|
619
1894
|
|
|
620
1895
|
function ensurePythonRuntime(home) {
|
|
621
1896
|
ensureDir(path.join(home, 'runtime'));
|
|
622
1897
|
ensureDir(path.join(home, 'runtime', 'bundle'));
|
|
623
|
-
|
|
1898
|
+
ensureDir(runtimeUvCachePath(home));
|
|
1899
|
+
ensureDir(runtimeUvPythonInstallPath(home));
|
|
1900
|
+
ensureDir(runtimeToolsPath(home));
|
|
1901
|
+
let uvBinary;
|
|
1902
|
+
try {
|
|
1903
|
+
uvBinary = ensureUvBinary(home);
|
|
1904
|
+
} catch (error) {
|
|
1905
|
+
printUvInstallGuidance(home, error instanceof Error ? error.message : String(error));
|
|
1906
|
+
process.exit(1);
|
|
1907
|
+
}
|
|
1908
|
+
const runtimePlan = resolvePythonRuntimePlan();
|
|
1909
|
+
if (runtimePlan.runtimeKind === 'managed') {
|
|
1910
|
+
printManagedPythonFallbackNotice({
|
|
1911
|
+
rejectedProbe: runtimePlan.rejectedProbe || null,
|
|
1912
|
+
source: runtimePlan.source,
|
|
1913
|
+
minimumVersionRequest: runtimePlan.minimumVersionRequest,
|
|
1914
|
+
installDir: runtimeUvPythonInstallPath(home),
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
const lockPath = ensureUvLockPresent();
|
|
624
1918
|
const stampPath = path.join(home, 'runtime', 'bundle', 'python-stamp.json');
|
|
1919
|
+
const editable = useEditableProjectInstall();
|
|
625
1920
|
const desiredStamp = {
|
|
1921
|
+
runtimeManager: 'uv',
|
|
626
1922
|
version: packageJson.version,
|
|
627
1923
|
pyprojectHash: sha256File(path.join(repoRoot, 'pyproject.toml')),
|
|
1924
|
+
uvLockHash: sha256File(lockPath),
|
|
1925
|
+
editable,
|
|
1926
|
+
sourceTreeHash: editable ? null : hashPythonSourceTree(),
|
|
1927
|
+
uvVersion: resolveUvVersion(uvBinary),
|
|
1928
|
+
envPath: runtimePythonEnvPath(home),
|
|
1929
|
+
source:
|
|
1930
|
+
runtimePlan.runtimeKind === 'system'
|
|
1931
|
+
? {
|
|
1932
|
+
kind: 'system',
|
|
1933
|
+
source: runtimePlan.selectedProbe.source,
|
|
1934
|
+
sourceExecutable:
|
|
1935
|
+
runtimePlan.selectedProbe.realExecutable
|
|
1936
|
+
|| runtimePlan.selectedProbe.executable
|
|
1937
|
+
|| runtimePlan.selectedProbe.binary,
|
|
1938
|
+
sourceVersion: runtimePlan.selectedProbe.version,
|
|
1939
|
+
sourceMajorMinor: pythonMajorMinor(runtimePlan.selectedProbe),
|
|
1940
|
+
}
|
|
1941
|
+
: {
|
|
1942
|
+
kind: 'uv-managed',
|
|
1943
|
+
minimumVersionRequest: runtimePlan.minimumVersionRequest,
|
|
1944
|
+
},
|
|
628
1945
|
};
|
|
629
1946
|
|
|
630
1947
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
if (
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
1948
|
+
const runtimePython = runtimePythonPath(home);
|
|
1949
|
+
const currentStamp = readJsonFile(stampPath);
|
|
1950
|
+
const runtimeProbe = fs.existsSync(runtimePython) ? probePython(runtimePython) : null;
|
|
1951
|
+
const runtimeBroken = !runtimeProbe || !runtimeProbe.ok || !pythonMeetsMinimum(runtimeProbe);
|
|
1952
|
+
const stampChanged = JSON.stringify(currentStamp || null) !== JSON.stringify(desiredStamp);
|
|
1953
|
+
|
|
1954
|
+
if (runtimeBroken || stampChanged) {
|
|
1955
|
+
const reason = runtimeBroken
|
|
1956
|
+
? 'DeepScientist is repairing the local uv-managed Python runtime.'
|
|
1957
|
+
: 'DeepScientist detected a runtime change and is rebuilding the local uv-managed environment.';
|
|
1958
|
+
console.warn(reason);
|
|
1959
|
+
fs.rmSync(stampPath, { force: true });
|
|
1960
|
+
fs.rmSync(runtimePythonEnvPath(home), { recursive: true, force: true });
|
|
1961
|
+
|
|
1962
|
+
let pythonTarget = null;
|
|
1963
|
+
let sourceLabel = null;
|
|
1964
|
+
if (runtimePlan.runtimeKind === 'system' && runtimePlan.selectedProbe) {
|
|
1965
|
+
pythonTarget =
|
|
1966
|
+
runtimePlan.selectedProbe.realExecutable
|
|
1967
|
+
|| runtimePlan.selectedProbe.executable
|
|
1968
|
+
|| runtimePlan.selectedProbe.binary;
|
|
1969
|
+
sourceLabel = `${runtimePlan.selectedProbe.sourceLabel} via uv-env`;
|
|
1970
|
+
step(1, 4, 'Preparing uv-managed Python runtime');
|
|
1971
|
+
} else {
|
|
1972
|
+
const managedPython = ensureUvManagedPython(home, uvBinary, runtimePlan.minimumVersionRequest);
|
|
1973
|
+
pythonTarget = managedPython.realExecutable || managedPython.executable || managedPython.binary;
|
|
1974
|
+
sourceLabel = managedPython.sourceLabel;
|
|
642
1975
|
}
|
|
643
|
-
}
|
|
644
1976
|
|
|
645
|
-
|
|
646
|
-
step(2, 4, 'Installing Python package and dependencies');
|
|
647
|
-
installPythonBundle(venvPython);
|
|
1977
|
+
syncUvProjectEnvironment(home, uvBinary, pythonTarget, editable);
|
|
648
1978
|
fs.writeFileSync(stampPath, `${JSON.stringify(desiredStamp, null, 2)}\n`, 'utf8');
|
|
1979
|
+
const syncedProbe = fs.existsSync(runtimePython) ? probePython(runtimePython) : null;
|
|
1980
|
+
if (syncedProbe && syncedProbe.ok && pythonMeetsMinimum(syncedProbe) && verifyPythonRuntime(runtimePython)) {
|
|
1981
|
+
fs.rmSync(legacyVenvRootPath(home), { recursive: true, force: true });
|
|
1982
|
+
return {
|
|
1983
|
+
runtimePython,
|
|
1984
|
+
uvBinary,
|
|
1985
|
+
runtimeManager: 'uv',
|
|
1986
|
+
runtimeProbe: createRuntimeSelectionProbe(syncedProbe, sourceLabel || 'uv-managed'),
|
|
1987
|
+
sourcePython: runtimePlan.selectedProbe || null,
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
649
1990
|
}
|
|
650
1991
|
|
|
651
|
-
if (verifyPythonRuntime(
|
|
652
|
-
|
|
1992
|
+
if (runtimeProbe && runtimeProbe.ok && pythonMeetsMinimum(runtimeProbe) && verifyPythonRuntime(runtimePython)) {
|
|
1993
|
+
fs.rmSync(legacyVenvRootPath(home), { recursive: true, force: true });
|
|
1994
|
+
return {
|
|
1995
|
+
runtimePython,
|
|
1996
|
+
uvBinary,
|
|
1997
|
+
runtimeManager: 'uv',
|
|
1998
|
+
runtimeProbe: createRuntimeSelectionProbe(
|
|
1999
|
+
runtimeProbe,
|
|
2000
|
+
runtimePlan.runtimeKind === 'system' && runtimePlan.selectedProbe
|
|
2001
|
+
? `${runtimePlan.selectedProbe.sourceLabel} via uv-env`
|
|
2002
|
+
: 'uv-managed'
|
|
2003
|
+
),
|
|
2004
|
+
sourcePython: runtimePlan.selectedProbe || null,
|
|
2005
|
+
};
|
|
653
2006
|
}
|
|
654
2007
|
|
|
655
|
-
console.warn('DeepScientist is
|
|
2008
|
+
console.warn('DeepScientist is retrying the local uv-managed Python runtime repair.');
|
|
656
2009
|
fs.rmSync(stampPath, { force: true });
|
|
657
|
-
fs.rmSync(
|
|
2010
|
+
fs.rmSync(runtimePythonEnvPath(home), { recursive: true, force: true });
|
|
658
2011
|
}
|
|
659
2012
|
|
|
660
|
-
console.error('DeepScientist could not prepare a healthy
|
|
2013
|
+
console.error('DeepScientist could not prepare a healthy uv-managed Python runtime.');
|
|
661
2014
|
process.exit(1);
|
|
662
2015
|
}
|
|
663
2016
|
|
|
664
|
-
function runPythonCli(
|
|
665
|
-
|
|
2017
|
+
function runPythonCli(runtimePython, args, options = {}) {
|
|
2018
|
+
const env = {
|
|
2019
|
+
...process.env,
|
|
2020
|
+
DEEPSCIENTIST_REPO_ROOT: repoRoot,
|
|
2021
|
+
...(options.env || {}),
|
|
2022
|
+
};
|
|
2023
|
+
return runSync(runtimePython, ['-m', 'deepscientist.cli', ...args], { ...options, env });
|
|
666
2024
|
}
|
|
667
2025
|
|
|
668
2026
|
function normalizePythonCliArgs(args, home) {
|
|
@@ -673,12 +2031,15 @@ function normalizePythonCliArgs(args, home) {
|
|
|
673
2031
|
index += 1;
|
|
674
2032
|
continue;
|
|
675
2033
|
}
|
|
2034
|
+
if (arg === '--hero' || arg === '--here') {
|
|
2035
|
+
continue;
|
|
2036
|
+
}
|
|
676
2037
|
normalized.push(arg);
|
|
677
2038
|
}
|
|
678
2039
|
return ['--home', home, ...normalized];
|
|
679
2040
|
}
|
|
680
2041
|
|
|
681
|
-
function ensureInitialized(home,
|
|
2042
|
+
function ensureInitialized(home, runtimePython) {
|
|
682
2043
|
const stampPath = path.join(home, 'runtime', 'bundle', 'init-stamp.json');
|
|
683
2044
|
let currentStamp = null;
|
|
684
2045
|
if (fs.existsSync(stampPath)) {
|
|
@@ -703,7 +2064,7 @@ function ensureInitialized(home, venvPython) {
|
|
|
703
2064
|
return;
|
|
704
2065
|
}
|
|
705
2066
|
step(3, 4, 'Preparing DeepScientist home, config, skills, and Git checks');
|
|
706
|
-
const result = runPythonCli(
|
|
2067
|
+
const result = runPythonCli(runtimePython, ['--home', home, 'init'], { capture: true, allowFailure: true });
|
|
707
2068
|
const stdout = result.stdout || '';
|
|
708
2069
|
let payload = {};
|
|
709
2070
|
try {
|
|
@@ -762,34 +2123,11 @@ function resolveExecutableOnPath(commandName) {
|
|
|
762
2123
|
return null;
|
|
763
2124
|
}
|
|
764
2125
|
const directories = pathValue.split(path.delimiter).filter(Boolean);
|
|
765
|
-
const extensions =
|
|
766
|
-
process.platform === 'win32'
|
|
767
|
-
? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM')
|
|
768
|
-
.split(';')
|
|
769
|
-
.filter(Boolean)
|
|
770
|
-
: [''];
|
|
771
2126
|
for (const directory of directories) {
|
|
772
2127
|
const base = path.join(directory, commandName);
|
|
773
|
-
for (const
|
|
774
|
-
|
|
775
|
-
try {
|
|
776
|
-
if (!fs.existsSync(candidate)) {
|
|
777
|
-
continue;
|
|
778
|
-
}
|
|
779
|
-
const stat = fs.statSync(candidate);
|
|
780
|
-
if (!stat.isFile()) {
|
|
781
|
-
continue;
|
|
782
|
-
}
|
|
783
|
-
if (process.platform !== 'win32') {
|
|
784
|
-
try {
|
|
785
|
-
fs.accessSync(candidate, fs.constants.X_OK);
|
|
786
|
-
} catch {
|
|
787
|
-
continue;
|
|
788
|
-
}
|
|
789
|
-
}
|
|
2128
|
+
for (const candidate of candidateExecutablePaths(base)) {
|
|
2129
|
+
if (isExecutableFile(candidate)) {
|
|
790
2130
|
return candidate;
|
|
791
|
-
} catch {
|
|
792
|
-
continue;
|
|
793
2131
|
}
|
|
794
2132
|
}
|
|
795
2133
|
}
|
|
@@ -1102,9 +2440,469 @@ async function stopDaemon(home) {
|
|
|
1102
2440
|
console.log('DeepScientist daemon stopped.');
|
|
1103
2441
|
}
|
|
1104
2442
|
|
|
1105
|
-
|
|
2443
|
+
function writeUpdateLog(home, content) {
|
|
2444
|
+
const logPath = path.join(home, 'logs', 'update.log');
|
|
2445
|
+
ensureDir(path.dirname(logPath));
|
|
2446
|
+
fs.appendFileSync(logPath, `${content.replace(/\s+$/, '')}\n`, 'utf8');
|
|
2447
|
+
return logPath;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
function summarizeUpdateFailure(result) {
|
|
2451
|
+
const lines = [];
|
|
2452
|
+
if (result.error) {
|
|
2453
|
+
lines.push(result.error);
|
|
2454
|
+
}
|
|
2455
|
+
if (result.stderr) {
|
|
2456
|
+
lines.push(String(result.stderr).trim());
|
|
2457
|
+
}
|
|
2458
|
+
if (result.stdout) {
|
|
2459
|
+
lines.push(String(result.stdout).trim());
|
|
2460
|
+
}
|
|
2461
|
+
return lines.filter(Boolean).join('\n').trim() || 'Unknown update failure.';
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
function runNpmInstallLatest(home, npmBinary) {
|
|
2465
|
+
const args = ['install', '-g', `${UPDATE_PACKAGE_NAME}@latest`, '--no-audit', '--no-fund'];
|
|
2466
|
+
const startedAt = new Date().toISOString();
|
|
2467
|
+
const result = spawnSync(npmBinary, args, {
|
|
2468
|
+
encoding: 'utf8',
|
|
2469
|
+
env: process.env,
|
|
2470
|
+
timeout: 15 * 60 * 1000,
|
|
2471
|
+
});
|
|
2472
|
+
const finishedAt = new Date().toISOString();
|
|
2473
|
+
const logPath = writeUpdateLog(
|
|
2474
|
+
home,
|
|
2475
|
+
[
|
|
2476
|
+
`=== ${startedAt} installing ${UPDATE_PACKAGE_NAME}@latest ===`,
|
|
2477
|
+
`$ ${npmBinary} ${args.join(' ')}`,
|
|
2478
|
+
String(result.stdout || '').trim(),
|
|
2479
|
+
String(result.stderr || '').trim(),
|
|
2480
|
+
`exit=${result.status ?? 'null'} error=${result.error ? result.error.message : 'none'}`,
|
|
2481
|
+
`=== finished ${finishedAt} ===`,
|
|
2482
|
+
'',
|
|
2483
|
+
].join('\n')
|
|
2484
|
+
);
|
|
2485
|
+
return {
|
|
2486
|
+
ok: !result.error && result.status === 0,
|
|
2487
|
+
stdout: String(result.stdout || ''),
|
|
2488
|
+
stderr: String(result.stderr || ''),
|
|
2489
|
+
error: result.error ? result.error.message : null,
|
|
2490
|
+
status: result.status ?? null,
|
|
2491
|
+
logPath,
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
async function promptUpdateAction(status) {
|
|
2496
|
+
const options = [
|
|
2497
|
+
{
|
|
2498
|
+
value: 'update',
|
|
2499
|
+
label: status.can_self_update ? 'Update now' : 'Show manual update',
|
|
2500
|
+
},
|
|
2501
|
+
{ value: 'later', label: 'Remind me later' },
|
|
2502
|
+
{ value: 'skip', label: 'Skip this version' },
|
|
2503
|
+
];
|
|
2504
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2505
|
+
return 'later';
|
|
2506
|
+
}
|
|
2507
|
+
return new Promise((resolve) => {
|
|
2508
|
+
let selected = 1;
|
|
2509
|
+
const lines = [
|
|
2510
|
+
'',
|
|
2511
|
+
'A new DeepScientist version is available.',
|
|
2512
|
+
'',
|
|
2513
|
+
`Current: ${status.current_version}`,
|
|
2514
|
+
`Latest: ${status.latest_version || 'unknown'}`,
|
|
2515
|
+
'',
|
|
2516
|
+
status.can_self_update
|
|
2517
|
+
? 'What do you want to do?'
|
|
2518
|
+
: 'Self-update is not available for this installation. Choose an action:',
|
|
2519
|
+
];
|
|
2520
|
+
|
|
2521
|
+
const cleanup = () => {
|
|
2522
|
+
process.stdin.off('keypress', onKeypress);
|
|
2523
|
+
if (process.stdin.isTTY) {
|
|
2524
|
+
process.stdin.setRawMode(false);
|
|
2525
|
+
}
|
|
2526
|
+
process.stdin.pause();
|
|
2527
|
+
console.log('');
|
|
2528
|
+
};
|
|
2529
|
+
|
|
2530
|
+
const render = () => {
|
|
2531
|
+
console.clear();
|
|
2532
|
+
for (const line of lines) {
|
|
2533
|
+
console.log(line);
|
|
2534
|
+
}
|
|
2535
|
+
for (let index = 0; index < options.length; index += 1) {
|
|
2536
|
+
const option = options[index];
|
|
2537
|
+
console.log(`${index === selected ? '>' : ' '} ${option.label}`);
|
|
2538
|
+
}
|
|
2539
|
+
console.log('');
|
|
2540
|
+
console.log('Use ↑/↓ and Enter.');
|
|
2541
|
+
};
|
|
2542
|
+
|
|
2543
|
+
const onKeypress = (_str, key) => {
|
|
2544
|
+
if (key?.name === 'up') {
|
|
2545
|
+
selected = (selected - 1 + options.length) % options.length;
|
|
2546
|
+
render();
|
|
2547
|
+
return;
|
|
2548
|
+
}
|
|
2549
|
+
if (key?.name === 'down') {
|
|
2550
|
+
selected = (selected + 1) % options.length;
|
|
2551
|
+
render();
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2554
|
+
if (key?.name === 'return') {
|
|
2555
|
+
const choice = options[selected].value;
|
|
2556
|
+
cleanup();
|
|
2557
|
+
resolve(choice);
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2560
|
+
if (key?.ctrl && key?.name === 'c') {
|
|
2561
|
+
cleanup();
|
|
2562
|
+
resolve('later');
|
|
2563
|
+
}
|
|
2564
|
+
};
|
|
2565
|
+
|
|
2566
|
+
readline.emitKeypressEvents(process.stdin);
|
|
2567
|
+
process.stdin.setRawMode(true);
|
|
2568
|
+
process.stdin.resume();
|
|
2569
|
+
process.stdin.on('keypress', onKeypress);
|
|
2570
|
+
render();
|
|
2571
|
+
});
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
function printUpdateStatus(status, { compact = false } = {}) {
|
|
2575
|
+
if (compact) {
|
|
2576
|
+
if (status.update_available) {
|
|
2577
|
+
console.log(
|
|
2578
|
+
`DeepScientist update available: ${status.current_version} -> ${status.latest_version}`
|
|
2579
|
+
);
|
|
2580
|
+
if (status.can_self_update) {
|
|
2581
|
+
console.log('Run `ds update --yes` to install it.');
|
|
2582
|
+
} else {
|
|
2583
|
+
console.log(`Manual update: ${status.manual_update_command}`);
|
|
2584
|
+
}
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
console.log(`DeepScientist is up to date (${status.current_version}).`);
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
console.log('DeepScientist update status');
|
|
2592
|
+
renderKeyValueRows([
|
|
2593
|
+
['Current', status.current_version],
|
|
2594
|
+
['Latest', status.latest_version || 'unknown'],
|
|
2595
|
+
['Available', status.update_available ? 'yes' : 'no'],
|
|
2596
|
+
['Install mode', status.install_mode],
|
|
2597
|
+
['Self-update', status.can_self_update ? 'supported' : 'manual-only'],
|
|
2598
|
+
['Last checked', status.last_checked_at || 'never'],
|
|
2599
|
+
]);
|
|
2600
|
+
if (status.last_check_error) {
|
|
2601
|
+
console.log('');
|
|
2602
|
+
console.log(`Version check error: ${status.last_check_error}`);
|
|
2603
|
+
}
|
|
2604
|
+
if (!status.can_self_update) {
|
|
2605
|
+
console.log('');
|
|
2606
|
+
console.log(`Manual update command: ${status.manual_update_command}`);
|
|
2607
|
+
if (status.reason) {
|
|
2608
|
+
console.log(status.reason);
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
const npmBinary = resolveNpmBinary();
|
|
2612
|
+
if (!npmBinary) {
|
|
2613
|
+
return {
|
|
2614
|
+
ok: false,
|
|
2615
|
+
updated: false,
|
|
2616
|
+
status,
|
|
2617
|
+
message: '`npm` is not available on PATH.',
|
|
2618
|
+
};
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
function spawnDetachedNode(args, options = {}) {
|
|
2623
|
+
const out = options.logPath ? fs.openSync(options.logPath, 'a') : 'ignore';
|
|
2624
|
+
const child = spawn(process.execPath, args, {
|
|
2625
|
+
cwd: options.cwd || repoRoot,
|
|
2626
|
+
detached: true,
|
|
2627
|
+
stdio: ['ignore', out, out],
|
|
2628
|
+
env: options.env || process.env,
|
|
2629
|
+
});
|
|
2630
|
+
child.unref();
|
|
2631
|
+
return child;
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
async function restartIntoUpdatedLauncher(rawArgs) {
|
|
2635
|
+
const launcherPath = resolveLauncherPath();
|
|
2636
|
+
if (!launcherPath) {
|
|
2637
|
+
throw new Error('Could not resolve the DeepScientist launcher after the update.');
|
|
2638
|
+
}
|
|
2639
|
+
const args = [launcherPath, '--skip-update-check', ...rawArgs.filter((item) => item !== '--skip-update-check')];
|
|
2640
|
+
const child = spawn(process.execPath, args, {
|
|
2641
|
+
cwd: repoRoot,
|
|
2642
|
+
stdio: 'inherit',
|
|
2643
|
+
env: process.env,
|
|
2644
|
+
});
|
|
2645
|
+
await new Promise((resolve, reject) => {
|
|
2646
|
+
child.on('error', reject);
|
|
2647
|
+
child.on('exit', (code) => {
|
|
2648
|
+
process.exit(code ?? 0);
|
|
2649
|
+
resolve();
|
|
2650
|
+
});
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
async function performSelfUpdate(home, options = {}) {
|
|
2655
|
+
const status = checkForUpdates(home, { force: true });
|
|
2656
|
+
if (!status.update_available) {
|
|
2657
|
+
return {
|
|
2658
|
+
ok: true,
|
|
2659
|
+
updated: false,
|
|
2660
|
+
status,
|
|
2661
|
+
message: `DeepScientist is already on the latest version (${status.current_version}).`,
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
2664
|
+
if (!status.can_self_update) {
|
|
2665
|
+
return {
|
|
2666
|
+
ok: false,
|
|
2667
|
+
updated: false,
|
|
2668
|
+
status,
|
|
2669
|
+
message: status.reason || `Manual update required: ${status.manual_update_command}`,
|
|
2670
|
+
};
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
const daemonState = readDaemonState(home);
|
|
2674
|
+
const configuredUi = readConfiguredUiAddressFromFile(home, options.host, options.port);
|
|
2675
|
+
const host = options.host || daemonState?.host || configuredUi.host;
|
|
2676
|
+
const port = options.port || daemonState?.port || configuredUi.port;
|
|
2677
|
+
const targetVersion = status.latest_version;
|
|
2678
|
+
|
|
2679
|
+
mergeUpdateState(home, {
|
|
2680
|
+
current_version: status.current_version,
|
|
2681
|
+
latest_version: targetVersion,
|
|
2682
|
+
target_version: targetVersion,
|
|
2683
|
+
busy: true,
|
|
2684
|
+
last_update_started_at: new Date().toISOString(),
|
|
2685
|
+
last_update_result: null,
|
|
2686
|
+
});
|
|
2687
|
+
|
|
2688
|
+
try {
|
|
2689
|
+
if (daemonState?.pid || daemonState?.daemon_id) {
|
|
2690
|
+
await stopDaemon(home);
|
|
2691
|
+
}
|
|
2692
|
+
} catch (error) {
|
|
2693
|
+
mergeUpdateState(home, {
|
|
2694
|
+
busy: false,
|
|
2695
|
+
last_update_finished_at: new Date().toISOString(),
|
|
2696
|
+
last_update_result: {
|
|
2697
|
+
ok: false,
|
|
2698
|
+
target_version: targetVersion,
|
|
2699
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2700
|
+
},
|
|
2701
|
+
});
|
|
2702
|
+
return {
|
|
2703
|
+
ok: false,
|
|
2704
|
+
updated: false,
|
|
2705
|
+
status,
|
|
2706
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2707
|
+
};
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
const installResult = runNpmInstallLatest(home, npmBinary);
|
|
2711
|
+
if (!installResult.ok) {
|
|
2712
|
+
const message = summarizeUpdateFailure(installResult);
|
|
2713
|
+
mergeUpdateState(home, {
|
|
2714
|
+
busy: false,
|
|
2715
|
+
last_update_finished_at: new Date().toISOString(),
|
|
2716
|
+
last_update_result: {
|
|
2717
|
+
ok: false,
|
|
2718
|
+
target_version: targetVersion,
|
|
2719
|
+
message,
|
|
2720
|
+
log_path: installResult.logPath,
|
|
2721
|
+
},
|
|
2722
|
+
});
|
|
2723
|
+
return {
|
|
2724
|
+
ok: false,
|
|
2725
|
+
updated: false,
|
|
2726
|
+
status,
|
|
2727
|
+
message,
|
|
2728
|
+
log_path: installResult.logPath,
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
const restartDaemon =
|
|
2733
|
+
options.restartDaemon === true
|
|
2734
|
+
|| (options.restartDaemon !== false && Boolean(daemonState?.pid || daemonState?.daemon_id));
|
|
2735
|
+
if (restartDaemon) {
|
|
2736
|
+
const launcherPath = resolveLauncherPath();
|
|
2737
|
+
if (!launcherPath) {
|
|
2738
|
+
const message = 'DeepScientist was updated, but the new launcher path could not be resolved for daemon restart.';
|
|
2739
|
+
mergeUpdateState(home, {
|
|
2740
|
+
busy: false,
|
|
2741
|
+
last_update_finished_at: new Date().toISOString(),
|
|
2742
|
+
last_update_result: {
|
|
2743
|
+
ok: false,
|
|
2744
|
+
target_version: targetVersion,
|
|
2745
|
+
message,
|
|
2746
|
+
log_path: installResult.logPath,
|
|
2747
|
+
},
|
|
2748
|
+
});
|
|
2749
|
+
return {
|
|
2750
|
+
ok: false,
|
|
2751
|
+
updated: true,
|
|
2752
|
+
status,
|
|
2753
|
+
message,
|
|
2754
|
+
log_path: installResult.logPath,
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
spawnDetachedNode(
|
|
2758
|
+
[
|
|
2759
|
+
launcherPath,
|
|
2760
|
+
'--home',
|
|
2761
|
+
home,
|
|
2762
|
+
'--host',
|
|
2763
|
+
String(host),
|
|
2764
|
+
'--port',
|
|
2765
|
+
String(port),
|
|
2766
|
+
'--daemon-only',
|
|
2767
|
+
'--no-browser',
|
|
2768
|
+
'--skip-update-check',
|
|
2769
|
+
],
|
|
2770
|
+
{
|
|
2771
|
+
cwd: repoRoot,
|
|
2772
|
+
env: process.env,
|
|
2773
|
+
logPath: path.join(home, 'logs', 'daemon-restart.log'),
|
|
2774
|
+
}
|
|
2775
|
+
);
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
mergeUpdateState(home, {
|
|
2779
|
+
busy: false,
|
|
2780
|
+
current_version: targetVersion,
|
|
2781
|
+
latest_version: targetVersion,
|
|
2782
|
+
target_version: null,
|
|
2783
|
+
last_checked_at: new Date().toISOString(),
|
|
2784
|
+
last_check_error: null,
|
|
2785
|
+
last_update_finished_at: new Date().toISOString(),
|
|
2786
|
+
last_update_result: {
|
|
2787
|
+
ok: true,
|
|
2788
|
+
target_version: targetVersion,
|
|
2789
|
+
message: restartDaemon
|
|
2790
|
+
? `DeepScientist updated to ${targetVersion}. The daemon is restarting.`
|
|
2791
|
+
: `DeepScientist updated to ${targetVersion}.`,
|
|
2792
|
+
log_path: installResult.logPath,
|
|
2793
|
+
},
|
|
2794
|
+
});
|
|
2795
|
+
|
|
2796
|
+
return {
|
|
2797
|
+
ok: true,
|
|
2798
|
+
updated: true,
|
|
2799
|
+
status: buildUpdateStatus(home),
|
|
2800
|
+
message: restartDaemon
|
|
2801
|
+
? `DeepScientist updated to ${targetVersion}. The daemon is restarting.`
|
|
2802
|
+
: `DeepScientist updated to ${targetVersion}.`,
|
|
2803
|
+
log_path: installResult.logPath,
|
|
2804
|
+
};
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
async function maybeHandleStartupUpdate(home, rawArgs, options = {}) {
|
|
2808
|
+
if (options.skipUpdateCheck || process.env.DS_SKIP_UPDATE_PROMPT === '1') {
|
|
2809
|
+
return false;
|
|
2810
|
+
}
|
|
2811
|
+
const status = checkForUpdates(home, { force: false });
|
|
2812
|
+
if (!status.update_available) {
|
|
2813
|
+
return false;
|
|
2814
|
+
}
|
|
2815
|
+
if (!status.prompt_recommended) {
|
|
2816
|
+
return false;
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2820
|
+
printUpdateStatus(status, { compact: true });
|
|
2821
|
+
mergeUpdateState(home, {
|
|
2822
|
+
last_prompted_at: new Date().toISOString(),
|
|
2823
|
+
});
|
|
2824
|
+
return false;
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
const action = await promptUpdateAction(status);
|
|
2828
|
+
if (action === 'later') {
|
|
2829
|
+
markUpdateDeferred(home, status.latest_version);
|
|
2830
|
+
return false;
|
|
2831
|
+
}
|
|
2832
|
+
if (action === 'skip') {
|
|
2833
|
+
markUpdateSkipped(home, status.latest_version);
|
|
2834
|
+
return false;
|
|
2835
|
+
}
|
|
2836
|
+
if (action === 'update' && !status.can_self_update) {
|
|
2837
|
+
console.log(`Manual update required: ${status.manual_update_command}`);
|
|
2838
|
+
if (status.reason) {
|
|
2839
|
+
console.log(status.reason);
|
|
2840
|
+
}
|
|
2841
|
+
markUpdateDeferred(home, status.latest_version);
|
|
2842
|
+
return false;
|
|
2843
|
+
}
|
|
2844
|
+
if (action === 'update') {
|
|
2845
|
+
console.log(`Updating DeepScientist ${status.current_version} -> ${status.latest_version} ...`);
|
|
2846
|
+
const result = await performSelfUpdate(home, { restartDaemon: false });
|
|
2847
|
+
if (!result.ok) {
|
|
2848
|
+
console.error(result.message);
|
|
2849
|
+
return false;
|
|
2850
|
+
}
|
|
2851
|
+
console.log(result.message);
|
|
2852
|
+
await restartIntoUpdatedLauncher(rawArgs);
|
|
2853
|
+
return true;
|
|
2854
|
+
}
|
|
2855
|
+
return false;
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
async function startBackgroundUpdateWorker(home, options = {}) {
|
|
2859
|
+
const launcherPath = resolveLauncherPath();
|
|
2860
|
+
if (!launcherPath) {
|
|
2861
|
+
return {
|
|
2862
|
+
ok: false,
|
|
2863
|
+
started: false,
|
|
2864
|
+
message: 'Could not resolve the launcher path for the background update worker.',
|
|
2865
|
+
};
|
|
2866
|
+
}
|
|
2867
|
+
const status = checkForUpdates(home, { force: false });
|
|
2868
|
+
mergeUpdateState(home, {
|
|
2869
|
+
current_version: status.current_version,
|
|
2870
|
+
latest_version: status.latest_version,
|
|
2871
|
+
target_version: status.latest_version,
|
|
2872
|
+
busy: true,
|
|
2873
|
+
last_update_started_at: new Date().toISOString(),
|
|
2874
|
+
last_update_result: null,
|
|
2875
|
+
});
|
|
2876
|
+
const workerArgs = [
|
|
2877
|
+
launcherPath,
|
|
2878
|
+
'update',
|
|
2879
|
+
'--yes',
|
|
2880
|
+
'--worker',
|
|
2881
|
+
'--home',
|
|
2882
|
+
home,
|
|
2883
|
+
'--host',
|
|
2884
|
+
String(options.host || '0.0.0.0'),
|
|
2885
|
+
'--port',
|
|
2886
|
+
String(options.port || 20999),
|
|
2887
|
+
'--restart-daemon',
|
|
2888
|
+
'--skip-update-check',
|
|
2889
|
+
];
|
|
2890
|
+
spawnDetachedNode(workerArgs, {
|
|
2891
|
+
cwd: repoRoot,
|
|
2892
|
+
env: process.env,
|
|
2893
|
+
logPath: path.join(home, 'logs', 'update-worker.log'),
|
|
2894
|
+
});
|
|
2895
|
+
return {
|
|
2896
|
+
ok: true,
|
|
2897
|
+
started: true,
|
|
2898
|
+
message: 'DeepScientist update worker started.',
|
|
2899
|
+
status: buildUpdateStatus(home),
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
async function readConfiguredUiAddress(home, runtimePython, fallbackHost, fallbackPort) {
|
|
1106
2904
|
try {
|
|
1107
|
-
const result = runPythonCli(
|
|
2905
|
+
const result = runPythonCli(runtimePython, ['--home', home, 'config', 'show', 'config'], { capture: true, allowFailure: true });
|
|
1108
2906
|
const text = result.stdout || '';
|
|
1109
2907
|
const hostMatch = text.match(/^\s*host:\s*["']?([^"'\n]+)["']?\s*$/m);
|
|
1110
2908
|
const portMatch = text.match(/^\s*port:\s*(\d+)\s*$/m);
|
|
@@ -1158,7 +2956,7 @@ function readConfiguredUiAddressFromFile(home, fallbackHost, fallbackPort) {
|
|
|
1158
2956
|
}
|
|
1159
2957
|
}
|
|
1160
2958
|
|
|
1161
|
-
async function startDaemon(home,
|
|
2959
|
+
async function startDaemon(home, runtimePython, host, port) {
|
|
1162
2960
|
const browserUrl = browserUiUrl(host, port);
|
|
1163
2961
|
const daemonBindUrl = bindUiUrl(host, port);
|
|
1164
2962
|
const state = readDaemonState(home);
|
|
@@ -1182,10 +2980,10 @@ async function startDaemon(home, venvPython, host, port) {
|
|
|
1182
2980
|
removeDaemonState(home);
|
|
1183
2981
|
}
|
|
1184
2982
|
|
|
1185
|
-
const bootstrapState = readCodexBootstrapState(home,
|
|
2983
|
+
const bootstrapState = readCodexBootstrapState(home, runtimePython);
|
|
1186
2984
|
if (!bootstrapState.codex_ready) {
|
|
1187
2985
|
console.log('Codex is not marked ready yet. Running startup probe...');
|
|
1188
|
-
const probe = probeCodexBootstrap(home,
|
|
2986
|
+
const probe = probeCodexBootstrap(home, runtimePython);
|
|
1189
2987
|
if (!probe || probe.ok !== true) {
|
|
1190
2988
|
throw createCodexPreflightError(home, probe);
|
|
1191
2989
|
}
|
|
@@ -1198,7 +2996,7 @@ async function startDaemon(home, venvPython, host, port) {
|
|
|
1198
2996
|
const out = fs.openSync(logPath, 'a');
|
|
1199
2997
|
const daemonId = crypto.randomUUID();
|
|
1200
2998
|
const child = spawn(
|
|
1201
|
-
|
|
2999
|
+
runtimePython,
|
|
1202
3000
|
['-m', 'deepscientist.cli', '--home', home, 'daemon', '--host', host, '--port', String(port)],
|
|
1203
3001
|
{
|
|
1204
3002
|
cwd: repoRoot,
|
|
@@ -1206,11 +3004,11 @@ async function startDaemon(home, venvPython, host, port) {
|
|
|
1206
3004
|
stdio: ['ignore', out, out],
|
|
1207
3005
|
env: {
|
|
1208
3006
|
...process.env,
|
|
3007
|
+
DEEPSCIENTIST_REPO_ROOT: repoRoot,
|
|
3008
|
+
DEEPSCIENTIST_NODE_BINARY: process.execPath,
|
|
3009
|
+
DEEPSCIENTIST_LAUNCHER_PATH: path.join(repoRoot, 'bin', 'ds.js'),
|
|
1209
3010
|
DS_DAEMON_ID: daemonId,
|
|
1210
3011
|
DS_DAEMON_MANAGED_BY: 'ds-launcher',
|
|
1211
|
-
PYTHONPATH: process.env.PYTHONPATH
|
|
1212
|
-
? `${srcPath}${path.delimiter}${process.env.PYTHONPATH}`
|
|
1213
|
-
: srcPath,
|
|
1214
3012
|
},
|
|
1215
3013
|
}
|
|
1216
3014
|
);
|
|
@@ -1304,7 +3102,7 @@ function handleCodexPreflightFailure(error) {
|
|
|
1304
3102
|
return true;
|
|
1305
3103
|
}
|
|
1306
3104
|
|
|
1307
|
-
function launchTui(url, questId, home,
|
|
3105
|
+
function launchTui(url, questId, home, runtimePython) {
|
|
1308
3106
|
const entry = ensureNodeBundle('src/tui', 'dist/index.js');
|
|
1309
3107
|
const args = [entry, '--base-url', url];
|
|
1310
3108
|
if (questId) {
|
|
@@ -1316,8 +3114,8 @@ function launchTui(url, questId, home, venvPython) {
|
|
|
1316
3114
|
env: {
|
|
1317
3115
|
...process.env,
|
|
1318
3116
|
DEEPSCIENTIST_TUI_HOME: home,
|
|
1319
|
-
DEEPSCIENTIST_TUI_PYTHON:
|
|
1320
|
-
|
|
3117
|
+
DEEPSCIENTIST_TUI_PYTHON: runtimePython,
|
|
3118
|
+
DEEPSCIENTIST_RUNTIME_PYTHON: runtimePython,
|
|
1321
3119
|
},
|
|
1322
3120
|
});
|
|
1323
3121
|
child.on('exit', (code) => {
|
|
@@ -1325,6 +3123,258 @@ function launchTui(url, questId, home, venvPython) {
|
|
|
1325
3123
|
});
|
|
1326
3124
|
}
|
|
1327
3125
|
|
|
3126
|
+
async function updateMain(rawArgs) {
|
|
3127
|
+
const options = parseUpdateArgs(rawArgs);
|
|
3128
|
+
if (!options) {
|
|
3129
|
+
printUpdateHelp();
|
|
3130
|
+
process.exit(1);
|
|
3131
|
+
}
|
|
3132
|
+
if (options.help) {
|
|
3133
|
+
printUpdateHelp();
|
|
3134
|
+
process.exit(0);
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
const home = options.home || resolveHome(rawArgs);
|
|
3138
|
+
ensureDir(home);
|
|
3139
|
+
|
|
3140
|
+
if (options.background && options.yes && !options.worker) {
|
|
3141
|
+
const payload = await startBackgroundUpdateWorker(home, {
|
|
3142
|
+
host: options.host,
|
|
3143
|
+
port: options.port,
|
|
3144
|
+
});
|
|
3145
|
+
if (options.json) {
|
|
3146
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
3147
|
+
} else {
|
|
3148
|
+
console.log(payload.message);
|
|
3149
|
+
}
|
|
3150
|
+
process.exit(payload.ok ? 0 : 1);
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
const status = checkForUpdates(home, { force: options.forceCheck || options.check || options.yes || options.worker });
|
|
3154
|
+
|
|
3155
|
+
if (options.remindLater) {
|
|
3156
|
+
const payload = markUpdateDeferred(home, status.latest_version);
|
|
3157
|
+
if (options.json) {
|
|
3158
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
3159
|
+
} else {
|
|
3160
|
+
console.log(`DeepScientist will remind you later about ${payload.latest_version || 'the next release'}.`);
|
|
3161
|
+
}
|
|
3162
|
+
process.exit(0);
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
if (options.skipVersion) {
|
|
3166
|
+
const payload = markUpdateSkipped(home, status.latest_version);
|
|
3167
|
+
if (options.json) {
|
|
3168
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
3169
|
+
} else {
|
|
3170
|
+
console.log(`DeepScientist will stop prompting for ${payload.last_skipped_version || payload.latest_version || 'this release'}.`);
|
|
3171
|
+
}
|
|
3172
|
+
process.exit(0);
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
if (options.worker) {
|
|
3176
|
+
const payload = await performSelfUpdate(home, {
|
|
3177
|
+
host: options.host,
|
|
3178
|
+
port: options.port,
|
|
3179
|
+
restartDaemon: options.restartDaemon,
|
|
3180
|
+
});
|
|
3181
|
+
if (options.json) {
|
|
3182
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
3183
|
+
} else {
|
|
3184
|
+
console.log(payload.message);
|
|
3185
|
+
}
|
|
3186
|
+
process.exit(payload.ok ? 0 : 1);
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
if (options.yes) {
|
|
3190
|
+
const payload = await performSelfUpdate(home, {
|
|
3191
|
+
host: options.host,
|
|
3192
|
+
port: options.port,
|
|
3193
|
+
restartDaemon: options.restartDaemon,
|
|
3194
|
+
});
|
|
3195
|
+
if (options.json) {
|
|
3196
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
3197
|
+
} else {
|
|
3198
|
+
console.log(payload.message);
|
|
3199
|
+
}
|
|
3200
|
+
process.exit(payload.ok ? 0 : 1);
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
if (options.check || options.json) {
|
|
3204
|
+
if (options.json) {
|
|
3205
|
+
console.log(JSON.stringify(status, null, 2));
|
|
3206
|
+
} else {
|
|
3207
|
+
printUpdateStatus(status);
|
|
3208
|
+
}
|
|
3209
|
+
process.exit(0);
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
if (!status.update_available) {
|
|
3213
|
+
printUpdateStatus(status, { compact: true });
|
|
3214
|
+
process.exit(0);
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
3218
|
+
printUpdateStatus(status, { compact: true });
|
|
3219
|
+
process.exit(0);
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
const action = await promptUpdateAction(status);
|
|
3223
|
+
if (action === 'later') {
|
|
3224
|
+
markUpdateDeferred(home, status.latest_version);
|
|
3225
|
+
console.log('Update reminder deferred.');
|
|
3226
|
+
process.exit(0);
|
|
3227
|
+
}
|
|
3228
|
+
if (action === 'skip') {
|
|
3229
|
+
markUpdateSkipped(home, status.latest_version);
|
|
3230
|
+
console.log(`Skipped ${status.latest_version}.`);
|
|
3231
|
+
process.exit(0);
|
|
3232
|
+
}
|
|
3233
|
+
if (!status.can_self_update) {
|
|
3234
|
+
console.log(`Manual update command: ${status.manual_update_command}`);
|
|
3235
|
+
if (status.reason) {
|
|
3236
|
+
console.log(status.reason);
|
|
3237
|
+
}
|
|
3238
|
+
process.exit(0);
|
|
3239
|
+
}
|
|
3240
|
+
const payload = await performSelfUpdate(home, {
|
|
3241
|
+
host: options.host,
|
|
3242
|
+
port: options.port,
|
|
3243
|
+
restartDaemon: options.restartDaemon,
|
|
3244
|
+
});
|
|
3245
|
+
console.log(payload.message);
|
|
3246
|
+
process.exit(payload.ok ? 0 : 1);
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
async function migrateMain(rawArgs) {
|
|
3250
|
+
const options = parseMigrateArgs(rawArgs);
|
|
3251
|
+
if (!options) {
|
|
3252
|
+
printMigrateHelp();
|
|
3253
|
+
process.exit(1);
|
|
3254
|
+
}
|
|
3255
|
+
if (options.help) {
|
|
3256
|
+
printMigrateHelp();
|
|
3257
|
+
process.exit(0);
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
const sourceHome = realpathOrSelf(options.home || resolveHome(rawArgs));
|
|
3261
|
+
const targetHome = path.resolve(options.target);
|
|
3262
|
+
if (!fs.existsSync(sourceHome)) {
|
|
3263
|
+
console.error(`DeepScientist source path does not exist: ${sourceHome}`);
|
|
3264
|
+
process.exit(1);
|
|
3265
|
+
}
|
|
3266
|
+
if (isPathEqual(sourceHome, targetHome)) {
|
|
3267
|
+
console.error('DeepScientist source and target paths are identical. Choose a different migration target.');
|
|
3268
|
+
process.exit(1);
|
|
3269
|
+
}
|
|
3270
|
+
if (isPathInside(targetHome, sourceHome) || isPathInside(sourceHome, targetHome)) {
|
|
3271
|
+
console.error('DeepScientist migration requires two separate sibling paths. Do not nest one path inside the other.');
|
|
3272
|
+
process.exit(1);
|
|
3273
|
+
}
|
|
3274
|
+
if (fs.existsSync(targetHome)) {
|
|
3275
|
+
console.error(`DeepScientist target path already exists: ${targetHome}`);
|
|
3276
|
+
process.exit(1);
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
printMigrationSummary({ sourceHome, targetHome, restart: options.restart });
|
|
3280
|
+
if (!options.yes) {
|
|
3281
|
+
const confirmed = await promptMigrationConfirmation({ sourceHome, targetHome });
|
|
3282
|
+
if (!confirmed) {
|
|
3283
|
+
console.log('DeepScientist migration cancelled.');
|
|
3284
|
+
process.exit(1);
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
const state = readDaemonState(sourceHome);
|
|
3289
|
+
const configured = readConfiguredUiAddressFromFile(sourceHome);
|
|
3290
|
+
const url = state?.url || browserUiUrl(configured.host, configured.port);
|
|
3291
|
+
const health = await fetchHealth(url);
|
|
3292
|
+
if (state || healthMatchesHome({ health, home: sourceHome })) {
|
|
3293
|
+
await stopDaemon(sourceHome);
|
|
3294
|
+
} else if (health && health.status === 'ok') {
|
|
3295
|
+
console.log(`Skipping daemon stop because ${url} belongs to another DeepScientist home.`);
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
const pythonRuntime = ensurePythonRuntime(sourceHome);
|
|
3299
|
+
const runtimePython = pythonRuntime.runtimePython;
|
|
3300
|
+
const result = runPythonCli(
|
|
3301
|
+
runtimePython,
|
|
3302
|
+
['--home', sourceHome, 'migrate', targetHome],
|
|
3303
|
+
{ capture: true, allowFailure: true }
|
|
3304
|
+
);
|
|
3305
|
+
let payload = null;
|
|
3306
|
+
try {
|
|
3307
|
+
payload = JSON.parse(String(result.stdout || '{}'));
|
|
3308
|
+
} catch {
|
|
3309
|
+
payload = null;
|
|
3310
|
+
}
|
|
3311
|
+
if (result.status !== 0 || !payload || payload.ok !== true) {
|
|
3312
|
+
if (result.stdout) {
|
|
3313
|
+
process.stdout.write(result.stdout);
|
|
3314
|
+
if (!String(result.stdout).endsWith('\n')) {
|
|
3315
|
+
process.stdout.write('\n');
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
if (result.stderr) {
|
|
3319
|
+
process.stderr.write(result.stderr);
|
|
3320
|
+
if (!String(result.stderr).endsWith('\n')) {
|
|
3321
|
+
process.stderr.write('\n');
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
console.error('DeepScientist migration failed.');
|
|
3325
|
+
process.exit(result.status ?? 1);
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
repairMigratedInstallWrappers(targetHome);
|
|
3329
|
+
const rewrittenWrappers = rewriteLauncherWrappersIfPointingAtSource({ sourceHome, targetHome });
|
|
3330
|
+
|
|
3331
|
+
const sourceContainsCurrentInstall = isPathEqual(repoRoot, path.join(sourceHome, 'cli')) || isPathInside(repoRoot, sourceHome);
|
|
3332
|
+
if (sourceContainsCurrentInstall) {
|
|
3333
|
+
scheduleDeferredSourceCleanup({ sourceHome, targetHome });
|
|
3334
|
+
} else {
|
|
3335
|
+
fs.rmSync(sourceHome, { recursive: true, force: true });
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
let restartMessage = 'Restart skipped.';
|
|
3339
|
+
if (options.restart) {
|
|
3340
|
+
const migratedLauncher = path.join(targetHome, 'cli', 'bin', 'ds.js');
|
|
3341
|
+
if (!fs.existsSync(migratedLauncher)) {
|
|
3342
|
+
restartMessage = `Migration succeeded, but restart was skipped because the migrated launcher is missing: ${migratedLauncher}`;
|
|
3343
|
+
} else {
|
|
3344
|
+
const child = spawn(
|
|
3345
|
+
process.execPath,
|
|
3346
|
+
[migratedLauncher, '--home', targetHome, '--daemon-only', '--no-browser', '--skip-update-check'],
|
|
3347
|
+
{
|
|
3348
|
+
cwd: path.join(targetHome, 'cli'),
|
|
3349
|
+
detached: true,
|
|
3350
|
+
stdio: 'ignore',
|
|
3351
|
+
env: process.env,
|
|
3352
|
+
}
|
|
3353
|
+
);
|
|
3354
|
+
child.unref();
|
|
3355
|
+
restartMessage = 'Managed daemon restart scheduled from the migrated home.';
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
console.log('');
|
|
3360
|
+
console.log('DeepScientist migration completed.');
|
|
3361
|
+
console.log(`New home: ${targetHome}`);
|
|
3362
|
+
if (payload.summary) {
|
|
3363
|
+
console.log(payload.summary);
|
|
3364
|
+
}
|
|
3365
|
+
if (rewrittenWrappers.length > 0) {
|
|
3366
|
+
console.log(`Updated wrappers: ${rewrittenWrappers.join(', ')}`);
|
|
3367
|
+
}
|
|
3368
|
+
console.log(restartMessage);
|
|
3369
|
+
if (sourceContainsCurrentInstall) {
|
|
3370
|
+
console.log(`Old path cleanup has been scheduled: ${sourceHome}`);
|
|
3371
|
+
} else {
|
|
3372
|
+
console.log(`Old path removed: ${sourceHome}`);
|
|
3373
|
+
}
|
|
3374
|
+
console.log(`Use \`ds --home ${targetHome}\` if you want to override the default explicitly.`);
|
|
3375
|
+
process.exit(0);
|
|
3376
|
+
}
|
|
3377
|
+
|
|
1328
3378
|
async function launcherMain(rawArgs) {
|
|
1329
3379
|
const options = parseLauncherArgs(rawArgs);
|
|
1330
3380
|
if (!options) {
|
|
@@ -1368,11 +3418,15 @@ async function launcherMain(rawArgs) {
|
|
|
1368
3418
|
process.exit(healthy && (!state || identityMatch) ? 0 : 1);
|
|
1369
3419
|
}
|
|
1370
3420
|
|
|
1371
|
-
const
|
|
1372
|
-
|
|
3421
|
+
const pythonRuntime = ensurePythonRuntime(home);
|
|
3422
|
+
const runtimePython = pythonRuntime.runtimePython;
|
|
3423
|
+
ensureInitialized(home, runtimePython);
|
|
3424
|
+
if (await maybeHandleStartupUpdate(home, rawArgs, options)) {
|
|
3425
|
+
return true;
|
|
3426
|
+
}
|
|
1373
3427
|
maybePrintOptionalLatexNotice(home);
|
|
1374
3428
|
|
|
1375
|
-
const configuredUi = await readConfiguredUiAddress(home,
|
|
3429
|
+
const configuredUi = await readConfiguredUiAddress(home, runtimePython, options.host, options.port);
|
|
1376
3430
|
const host = configuredUi.host;
|
|
1377
3431
|
const port = configuredUi.port;
|
|
1378
3432
|
const mode = normalizeMode(options.mode ?? 'web');
|
|
@@ -1388,7 +3442,7 @@ async function launcherMain(rawArgs) {
|
|
|
1388
3442
|
step(4, 4, 'Starting local daemon and UI surfaces');
|
|
1389
3443
|
let started;
|
|
1390
3444
|
try {
|
|
1391
|
-
started = await startDaemon(home,
|
|
3445
|
+
started = await startDaemon(home, runtimePython, host, port);
|
|
1392
3446
|
} catch (error) {
|
|
1393
3447
|
if (handleCodexPreflightFailure(error)) return true;
|
|
1394
3448
|
throw error;
|
|
@@ -1401,6 +3455,8 @@ async function launcherMain(rawArgs) {
|
|
|
1401
3455
|
autoOpenRequested: shouldOpenBrowser,
|
|
1402
3456
|
browserOpened,
|
|
1403
3457
|
daemonOnly: options.daemonOnly,
|
|
3458
|
+
home,
|
|
3459
|
+
pythonSelection: pythonRuntime.runtimeProbe,
|
|
1404
3460
|
});
|
|
1405
3461
|
|
|
1406
3462
|
if (options.daemonOnly) {
|
|
@@ -1409,13 +3465,21 @@ async function launcherMain(rawArgs) {
|
|
|
1409
3465
|
if (mode === 'web') {
|
|
1410
3466
|
process.exit(0);
|
|
1411
3467
|
}
|
|
1412
|
-
launchTui(started.url, options.questId, home,
|
|
3468
|
+
launchTui(started.url, options.questId, home, runtimePython);
|
|
1413
3469
|
return true;
|
|
1414
3470
|
}
|
|
1415
3471
|
|
|
1416
3472
|
async function main() {
|
|
1417
3473
|
const args = process.argv.slice(2);
|
|
1418
3474
|
const positional = findFirstPositionalArg(args);
|
|
3475
|
+
if (positional && positional.value === 'update') {
|
|
3476
|
+
await updateMain(args);
|
|
3477
|
+
return;
|
|
3478
|
+
}
|
|
3479
|
+
if (positional && positional.value === 'migrate') {
|
|
3480
|
+
await migrateMain(args);
|
|
3481
|
+
return;
|
|
3482
|
+
}
|
|
1419
3483
|
if (args.length === 0 || args[0] === 'ui' || (!positional && args[0]?.startsWith('--'))) {
|
|
1420
3484
|
await launcherMain(args);
|
|
1421
3485
|
return;
|
|
@@ -1426,15 +3490,16 @@ async function main() {
|
|
|
1426
3490
|
}
|
|
1427
3491
|
if (positional && pythonCommands.has(positional.value)) {
|
|
1428
3492
|
const home = resolveHome(args);
|
|
1429
|
-
const
|
|
3493
|
+
const pythonRuntime = ensurePythonRuntime(home);
|
|
3494
|
+
const runtimePython = pythonRuntime.runtimePython;
|
|
1430
3495
|
if (positional.value === 'run' || positional.value === 'daemon') {
|
|
1431
3496
|
maybePrintOptionalLatexNotice(home);
|
|
1432
3497
|
}
|
|
1433
3498
|
if (positional.value === 'run' || positional.value === 'daemon') {
|
|
1434
|
-
const bootstrapState = readCodexBootstrapState(home,
|
|
3499
|
+
const bootstrapState = readCodexBootstrapState(home, runtimePython);
|
|
1435
3500
|
if (!bootstrapState.codex_ready) {
|
|
1436
3501
|
try {
|
|
1437
|
-
const probe = probeCodexBootstrap(home,
|
|
3502
|
+
const probe = probeCodexBootstrap(home, runtimePython);
|
|
1438
3503
|
if (!probe || probe.ok !== true) {
|
|
1439
3504
|
throw createCodexPreflightError(home, probe);
|
|
1440
3505
|
}
|
|
@@ -1444,14 +3509,35 @@ async function main() {
|
|
|
1444
3509
|
}
|
|
1445
3510
|
}
|
|
1446
3511
|
}
|
|
1447
|
-
const result = runPythonCli(
|
|
3512
|
+
const result = runPythonCli(runtimePython, normalizePythonCliArgs(args, home), { allowFailure: true });
|
|
1448
3513
|
process.exit(result.status ?? 0);
|
|
1449
3514
|
return;
|
|
1450
3515
|
}
|
|
1451
3516
|
await launcherMain(args);
|
|
1452
3517
|
}
|
|
1453
3518
|
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
3519
|
+
module.exports = {
|
|
3520
|
+
__internal: {
|
|
3521
|
+
minimumPythonRequest,
|
|
3522
|
+
createPythonRuntimePlan,
|
|
3523
|
+
buildUvRuntimeEnv,
|
|
3524
|
+
runtimePythonEnvPath,
|
|
3525
|
+
runtimePythonPath,
|
|
3526
|
+
runtimeUvBinaryPath,
|
|
3527
|
+
legacyVenvRootPath,
|
|
3528
|
+
resolveUvBinary,
|
|
3529
|
+
resolveHome,
|
|
3530
|
+
parseMigrateArgs,
|
|
3531
|
+
useEditableProjectInstall,
|
|
3532
|
+
compareVersions,
|
|
3533
|
+
detectInstallMode,
|
|
3534
|
+
buildUpdateStatus,
|
|
3535
|
+
},
|
|
3536
|
+
};
|
|
3537
|
+
|
|
3538
|
+
if (require.main === module) {
|
|
3539
|
+
main().catch((error) => {
|
|
3540
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
3541
|
+
process.exit(1);
|
|
3542
|
+
});
|
|
3543
|
+
}
|