@researai/deepscientist 1.5.0 → 1.5.2
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/AGENTS.md +26 -0
- package/README.md +47 -161
- package/assets/connectors/lingzhu/openclaw-bridge/README.md +124 -0
- package/assets/connectors/lingzhu/openclaw-bridge/index.ts +162 -0
- package/assets/connectors/lingzhu/openclaw-bridge/openclaw.plugin.json +145 -0
- package/assets/connectors/lingzhu/openclaw-bridge/package.json +35 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/cli.ts +180 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/config.ts +196 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/debug-log.ts +111 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/events.ts +4 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/http-handler.ts +1133 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/image-cache.ts +75 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/lingzhu-tools.ts +246 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/transform.ts +541 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/types.ts +131 -0
- package/assets/connectors/lingzhu/openclaw-bridge/tsconfig.json +14 -0
- package/assets/connectors/lingzhu/openclaw.lingzhu.config.template.json +39 -0
- package/bin/ds.js +2048 -166
- package/docs/en/00_QUICK_START.md +152 -0
- package/docs/en/01_SETTINGS_REFERENCE.md +1104 -0
- package/docs/en/02_START_RESEARCH_GUIDE.md +404 -0
- package/docs/en/03_QQ_CONNECTOR_GUIDE.md +325 -0
- package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +216 -0
- package/docs/en/05_TUI_GUIDE.md +141 -0
- package/docs/en/06_RUNTIME_AND_CANVAS.md +679 -0
- package/docs/en/07_MEMORY_AND_MCP.md +253 -0
- package/docs/en/08_FIGURE_STYLE_GUIDE.md +97 -0
- package/docs/en/09_DOCTOR.md +152 -0
- package/docs/en/90_ARCHITECTURE.md +247 -0
- package/docs/en/91_DEVELOPMENT.md +195 -0
- package/docs/en/99_ACKNOWLEDGEMENTS.md +29 -0
- package/docs/zh/00_QUICK_START.md +152 -0
- package/docs/zh/01_SETTINGS_REFERENCE.md +1137 -0
- package/docs/zh/02_START_RESEARCH_GUIDE.md +414 -0
- package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +324 -0
- package/docs/zh/04_LINGZHU_CONNECTOR_GUIDE.md +230 -0
- package/docs/zh/05_TUI_GUIDE.md +128 -0
- package/docs/zh/06_RUNTIME_AND_CANVAS.md +271 -0
- package/docs/zh/07_MEMORY_AND_MCP.md +235 -0
- package/docs/zh/08_FIGURE_STYLE_GUIDE.md +97 -0
- package/docs/zh/09_DOCTOR.md +154 -0
- package/docs/zh/99_ACKNOWLEDGEMENTS.md +29 -0
- package/install.sh +41 -16
- package/package.json +5 -2
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +6 -1
- package/src/deepscientist/artifact/guidance.py +9 -2
- package/src/deepscientist/artifact/service.py +1026 -39
- package/src/deepscientist/bash_exec/monitor.py +27 -5
- package/src/deepscientist/bash_exec/runtime.py +639 -0
- package/src/deepscientist/bash_exec/service.py +99 -16
- package/src/deepscientist/bridges/base.py +3 -0
- package/src/deepscientist/bridges/connectors.py +292 -13
- package/src/deepscientist/channels/qq.py +19 -2
- package/src/deepscientist/channels/relay.py +1 -0
- package/src/deepscientist/cli.py +32 -25
- package/src/deepscientist/config/models.py +28 -2
- package/src/deepscientist/config/service.py +202 -7
- package/src/deepscientist/connector_runtime.py +2 -0
- package/src/deepscientist/daemon/api/handlers.py +68 -6
- package/src/deepscientist/daemon/api/router.py +3 -0
- package/src/deepscientist/daemon/app.py +531 -15
- package/src/deepscientist/doctor.py +511 -0
- package/src/deepscientist/gitops/diff.py +3 -0
- package/src/deepscientist/home.py +26 -2
- package/src/deepscientist/latex_runtime.py +17 -4
- package/src/deepscientist/lingzhu_support.py +182 -0
- package/src/deepscientist/mcp/context.py +3 -1
- package/src/deepscientist/mcp/server.py +55 -2
- package/src/deepscientist/prompts/builder.py +222 -58
- package/src/deepscientist/quest/layout.py +2 -0
- package/src/deepscientist/quest/service.py +133 -14
- package/src/deepscientist/quest/stage_views.py +65 -1
- package/src/deepscientist/runners/codex.py +2 -0
- package/src/deepscientist/runtime_tools/__init__.py +16 -0
- package/src/deepscientist/runtime_tools/builtins.py +19 -0
- package/src/deepscientist/runtime_tools/models.py +29 -0
- package/src/deepscientist/runtime_tools/registry.py +40 -0
- package/src/deepscientist/runtime_tools/service.py +59 -0
- package/src/deepscientist/runtime_tools/tinytex.py +25 -0
- package/src/deepscientist/shared.py +44 -17
- package/src/deepscientist/tinytex.py +276 -0
- package/src/prompts/connectors/lingzhu.md +15 -0
- package/src/prompts/connectors/qq.md +121 -0
- package/src/prompts/system.md +214 -37
- package/src/skills/analysis-campaign/SKILL.md +46 -7
- package/src/skills/baseline/SKILL.md +12 -5
- package/src/skills/decision/SKILL.md +7 -5
- package/src/skills/experiment/SKILL.md +22 -5
- package/src/skills/finalize/SKILL.md +9 -5
- package/src/skills/idea/SKILL.md +6 -5
- package/src/skills/intake-audit/SKILL.md +277 -0
- package/src/skills/intake-audit/references/state-audit-template.md +41 -0
- package/src/skills/rebuttal/SKILL.md +409 -0
- package/src/skills/rebuttal/references/action-plan-template.md +63 -0
- package/src/skills/rebuttal/references/evidence-update-template.md +30 -0
- package/src/skills/rebuttal/references/response-letter-template.md +113 -0
- package/src/skills/rebuttal/references/review-matrix-template.md +55 -0
- package/src/skills/review/SKILL.md +295 -0
- package/src/skills/review/references/experiment-todo-template.md +29 -0
- package/src/skills/review/references/review-report-template.md +83 -0
- package/src/skills/review/references/revision-log-template.md +40 -0
- package/src/skills/scout/SKILL.md +6 -5
- package/src/skills/write/SKILL.md +8 -4
- package/src/tui/dist/components/WelcomePanel.js +17 -43
- package/src/tui/dist/components/messages/BashExecOperationMessage.js +3 -2
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-7v-dHngU.js → AiManusChatView-CZpg376x.js} +127 -597
- package/src/ui/dist/assets/{AnalysisPlugin-B_Xmz-KE.js → AnalysisPlugin-CtHA22g3.js} +1 -1
- package/src/ui/dist/assets/{AutoFigurePlugin-Cko-0tm1.js → AutoFigurePlugin-BSWmLMmF.js} +63 -8
- package/src/ui/dist/assets/{CliPlugin-BsU0ht7q.js → CliPlugin-CJ7jdm_s.js} +43 -609
- package/src/ui/dist/assets/{CodeEditorPlugin-DcMMP0Rt.js → CodeEditorPlugin-DhInVGFf.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-BqoQ5QyY.js → CodeViewerPlugin-D1n8S9r5.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-D7eHNhU6.js → DocViewerPlugin-C4XM_kqk.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-DLJN42T5.js → GitDiffViewerPlugin-W6kS9r6v.js} +1 -1
- package/src/ui/dist/assets/{ImageViewerPlugin-gJMV7MOu.js → ImageViewerPlugin-DPeUx_Oz.js} +5 -6
- package/src/ui/dist/assets/{LabCopilotPanel-B857sfxP.js → LabCopilotPanel-eAelUaub.js} +12 -15
- package/src/ui/dist/assets/LabPlugin-BbOrBxKY.js +2676 -0
- package/src/ui/dist/assets/{LatexPlugin-DWKEo-Wj.js → LatexPlugin-C-HhkVXY.js} +16 -16
- package/src/ui/dist/assets/{MarkdownViewerPlugin-DBzoEmhv.js → MarkdownViewerPlugin-BDIzIBfh.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-DoHc-8vo.js → MarketplacePlugin-DAOJphwr.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-CKjKH-yS.js → NotebookEditor-BsoMvDoU.js} +3 -3
- package/src/ui/dist/assets/{PdfLoader-zFoL0VPo.js → PdfLoader-fiC7RtHf.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-DXPaL9Nt.js → PdfMarkdownPlugin-C5OxZBFK.js} +3 -3
- package/src/ui/dist/assets/{PdfViewerPlugin-DhK8qCFp.js → PdfViewerPlugin-CAbxQebk.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-CdSi6krf.js → SearchPlugin-SE33Lb9B.js} +1 -1
- package/src/ui/dist/assets/{Stepper-V-WiDQJl.js → Stepper-0Av7GfV7.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-hIs1Efiu.js → TextViewerPlugin-Daf2gJDI.js} +4 -4
- package/src/ui/dist/assets/{VNCViewer-DG8b0q2X.js → VNCViewer-BKrMUIOX.js} +9 -10
- package/src/ui/dist/assets/{bibtex-HDac6fVW.js → bibtex-JBdOEe45.js} +1 -1
- package/src/ui/dist/assets/{code-BnBeNxBc.js → code-B0TDFCZz.js} +1 -1
- package/src/ui/dist/assets/{file-content-IRQ3jHb8.js → file-content-3YtrSacz.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-DZoQ9I6r.js → file-diff-panel-CJEg5OG1.js} +1 -1
- package/src/ui/dist/assets/{file-socket-BMCdLc-P.js → file-socket-CYQYdmB1.js} +1 -1
- package/src/ui/dist/assets/{file-utils-CltILB3w.js → file-utils-Cd1C9Ppl.js} +1 -1
- package/src/ui/dist/assets/{image-Boe6ffhu.js → image-B33ctrvC.js} +1 -1
- package/src/ui/dist/assets/{index-2Zf65FZt.js → index-9CLPVeZh.js} +1 -1
- package/src/ui/dist/assets/{index-DZqJ-qAM.js → index-BNQWqmJ2.js} +60 -2154
- package/src/ui/dist/assets/{index-DO43pFZP.js → index-BVXsmS7V.js} +84086 -84365
- package/src/ui/dist/assets/{index-BlplpvE1.js → index-Buw_N1VQ.js} +2 -2
- package/src/ui/dist/assets/{index-Bq2bvfkl.css → index-SwmFAld3.css} +2622 -2619
- package/src/ui/dist/assets/{message-square-mUHn_Ssb.js → message-square-D0cUJ9yU.js} +1 -1
- package/src/ui/dist/assets/{monaco-fe0arNEU.js → monaco-UZLYkp2n.js} +1 -1
- package/src/ui/dist/assets/{popover-D_7i19qU.js → popover-CTeiY-dK.js} +1 -1
- package/src/ui/dist/assets/{project-sync-DyVGrU7H.js → project-sync-Dbs01Xky.js} +2 -8
- package/src/ui/dist/assets/{sigma-BzazRyxQ.js → sigma-CM08S-xT.js} +1 -1
- package/src/ui/dist/assets/{tooltip-DN_yjHFH.js → tooltip-pDtzvU9p.js} +1 -1
- package/src/ui/dist/assets/trash-YvPCP-da.js +32 -0
- package/src/ui/dist/assets/{useCliAccess-DV2L2Qxy.js → useCliAccess-Bavi74Ac.js} +12 -42
- package/src/ui/dist/assets/{useFileDiffOverlay-DyTj-p_V.js → useFileDiffOverlay-CVXY6oeg.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-ozYHtUwq.js → wrap-text-Cf4flRW7.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-BN9MUyCQ.js → zoom-out-Hb0Z1YpT.js} +1 -1
- package/src/ui/dist/index.html +2 -2
- package/uv.lock +1155 -0
- package/assets/fonts/Inter-Variable.ttf +0 -0
- package/assets/fonts/NotoSerifSC-Regular-C94HN_ZN.ttf +0 -0
- package/assets/fonts/NunitoSans-Variable.ttf +0 -0
- package/assets/fonts/Satoshi-Medium-ByP-Zb-9.woff2 +0 -0
- package/assets/fonts/SourceSans3-Variable.ttf +0 -0
- package/assets/fonts/ds-fonts.css +0 -83
- package/src/ui/dist/assets/Inter-Variable-VF2RPR_K.ttf +0 -0
- package/src/ui/dist/assets/LabPlugin-bL7rpic8.js +0 -43
- package/src/ui/dist/assets/NotoSerifSC-Regular-C94HN_ZN-C94HN_ZN.ttf +0 -0
- package/src/ui/dist/assets/NunitoSans-Variable-B_ZymHAd.ttf +0 -0
- package/src/ui/dist/assets/Satoshi-Medium-ByP-Zb-9-GkA34YXu.woff2 +0 -0
- package/src/ui/dist/assets/SourceSans3-Variable-CD-WOsSK.ttf +0 -0
- package/src/ui/dist/assets/info-CcsK_htA.js +0 -18
- package/src/ui/dist/assets/user-plus-BusDx-hF.js +0 -79
package/bin/ds.js
CHANGED
|
@@ -3,13 +3,16 @@ 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);
|
|
13
16
|
const pythonCommands = new Set([
|
|
14
17
|
'init',
|
|
15
18
|
'new',
|
|
@@ -21,12 +24,17 @@ const pythonCommands = new Set([
|
|
|
21
24
|
'note',
|
|
22
25
|
'approve',
|
|
23
26
|
'graph',
|
|
24
|
-
'
|
|
27
|
+
'doctor',
|
|
28
|
+
'docker',
|
|
25
29
|
'push',
|
|
26
30
|
'memory',
|
|
27
31
|
'baseline',
|
|
32
|
+
'latex',
|
|
28
33
|
'config',
|
|
29
34
|
]);
|
|
35
|
+
const UPDATE_PACKAGE_NAME = String(packageJson.name || '@researai/deepscientist').trim() || '@researai/deepscientist';
|
|
36
|
+
const UPDATE_CHECK_TTL_MS = 12 * 60 * 60 * 1000;
|
|
37
|
+
const UPDATE_PROMPT_TTL_MS = 12 * 60 * 60 * 1000;
|
|
30
38
|
|
|
31
39
|
const optionsWithValues = new Set(['--home', '--host', '--port', '--quest-id', '--mode']);
|
|
32
40
|
|
|
@@ -34,17 +42,50 @@ function printLauncherHelp() {
|
|
|
34
42
|
console.log(`DeepScientist launcher
|
|
35
43
|
|
|
36
44
|
Usage:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
ds
|
|
46
|
+
ds update
|
|
47
|
+
ds update --check
|
|
48
|
+
ds update --yes
|
|
49
|
+
ds --tui
|
|
50
|
+
ds --both
|
|
51
|
+
ds --host 0.0.0.0 --port 21000
|
|
52
|
+
ds --stop
|
|
53
|
+
ds --restart
|
|
54
|
+
ds --status
|
|
55
|
+
ds doctor
|
|
56
|
+
ds latex status
|
|
57
|
+
ds --home ~/DeepScientist --port 20999
|
|
58
|
+
|
|
59
|
+
Launcher flags:
|
|
60
|
+
--host <host> Bind host for the local web daemon
|
|
61
|
+
--port <port> Bind port for the local web daemon
|
|
62
|
+
--tui Start the terminal workspace only
|
|
63
|
+
--both Start web + terminal workspace together
|
|
64
|
+
--no-browser Do not auto-open the browser
|
|
65
|
+
--daemon-only Start the managed daemon and exit
|
|
66
|
+
--status Print managed daemon health as JSON
|
|
67
|
+
--stop Stop the managed daemon
|
|
68
|
+
--restart Restart the managed daemon
|
|
69
|
+
--home <path> Use a custom DeepScientist home
|
|
70
|
+
--quest-id <id> Open the TUI on one quest directly
|
|
71
|
+
|
|
72
|
+
Update:
|
|
73
|
+
ds update Check the npm package version and offer update actions
|
|
74
|
+
ds update --check Print structured update status
|
|
75
|
+
ds update --yes Install the latest npm release immediately
|
|
76
|
+
|
|
77
|
+
Runtime:
|
|
78
|
+
DeepScientist uses uv to manage a locked local Python runtime.
|
|
79
|
+
If uv is missing, ds bootstraps a local copy under the DeepScientist home automatically.
|
|
80
|
+
If an active conda environment provides Python ${requiredPythonSpec}, ds prefers it.
|
|
81
|
+
Otherwise uv provisions a managed Python under the DeepScientist home automatically.
|
|
43
82
|
|
|
44
83
|
Advanced Python CLI:
|
|
45
84
|
ds init
|
|
46
|
-
ds new "reproduce baseline and test idea"
|
|
47
|
-
ds
|
|
85
|
+
ds new "reproduce baseline and test one stronger idea"
|
|
86
|
+
ds doctor
|
|
87
|
+
ds latex install-runtime
|
|
88
|
+
ds run decision --quest-id 001 --message "review current state"
|
|
48
89
|
`);
|
|
49
90
|
}
|
|
50
91
|
|
|
@@ -52,6 +93,292 @@ function ensureDir(targetPath) {
|
|
|
52
93
|
fs.mkdirSync(targetPath, { recursive: true });
|
|
53
94
|
}
|
|
54
95
|
|
|
96
|
+
function expandUserPath(rawPath) {
|
|
97
|
+
const normalized = String(rawPath || '').trim();
|
|
98
|
+
if (!normalized) {
|
|
99
|
+
return normalized;
|
|
100
|
+
}
|
|
101
|
+
if (normalized === '~') {
|
|
102
|
+
return os.homedir();
|
|
103
|
+
}
|
|
104
|
+
if (normalized.startsWith(`~${path.sep}`) || normalized.startsWith('~/')) {
|
|
105
|
+
return path.join(os.homedir(), normalized.slice(2));
|
|
106
|
+
}
|
|
107
|
+
return normalized;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function updateStatePath(home) {
|
|
111
|
+
return path.join(home, 'runtime', 'update-state.json');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function readUpdateState(home) {
|
|
115
|
+
return readJsonFile(updateStatePath(home)) || {};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function writeUpdateState(home, payload) {
|
|
119
|
+
const statePath = updateStatePath(home);
|
|
120
|
+
ensureDir(path.dirname(statePath));
|
|
121
|
+
fs.writeFileSync(statePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function mergeUpdateState(home, patch) {
|
|
125
|
+
const current = readUpdateState(home);
|
|
126
|
+
const next = {
|
|
127
|
+
...current,
|
|
128
|
+
...patch,
|
|
129
|
+
};
|
|
130
|
+
writeUpdateState(home, next);
|
|
131
|
+
return next;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseTimestamp(value) {
|
|
135
|
+
if (!value) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const parsed = Date.parse(String(value));
|
|
139
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isExpired(value, ttlMs) {
|
|
143
|
+
const parsed = parseTimestamp(value);
|
|
144
|
+
if (parsed === null) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
return Date.now() - parsed > ttlMs;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function normalizeVersion(value) {
|
|
151
|
+
return String(value || '')
|
|
152
|
+
.trim()
|
|
153
|
+
.replace(/^v/i, '');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function compareVersions(left, right) {
|
|
157
|
+
const leftParts = normalizeVersion(left).split('.').map((item) => Number.parseInt(item, 10) || 0);
|
|
158
|
+
const rightParts = normalizeVersion(right).split('.').map((item) => Number.parseInt(item, 10) || 0);
|
|
159
|
+
const length = Math.max(leftParts.length, rightParts.length, 3);
|
|
160
|
+
for (let index = 0; index < length; index += 1) {
|
|
161
|
+
const leftValue = leftParts[index] || 0;
|
|
162
|
+
const rightValue = rightParts[index] || 0;
|
|
163
|
+
if (leftValue > rightValue) {
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
if (leftValue < rightValue) {
|
|
167
|
+
return -1;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function detectInstallMode(rootPath = repoRoot) {
|
|
174
|
+
const normalized = String(rootPath || '');
|
|
175
|
+
return normalized.includes(`${path.sep}node_modules${path.sep}`) ? 'npm-package' : 'source-checkout';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function updateManualCommand(installMode) {
|
|
179
|
+
if (installMode === 'npm-package') {
|
|
180
|
+
return `npm install -g ${UPDATE_PACKAGE_NAME}@latest`;
|
|
181
|
+
}
|
|
182
|
+
return 'git pull && bash install.sh';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function updateSupportSummary(installMode, npmBinary, launcherPath) {
|
|
186
|
+
if (!npmBinary) {
|
|
187
|
+
return {
|
|
188
|
+
canCheck: false,
|
|
189
|
+
canSelfUpdate: false,
|
|
190
|
+
reason: '`npm` is not available on PATH.',
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (installMode !== 'npm-package') {
|
|
194
|
+
return {
|
|
195
|
+
canCheck: true,
|
|
196
|
+
canSelfUpdate: false,
|
|
197
|
+
reason: 'This DeepScientist installation comes from a source checkout and should be updated from Git.',
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (!launcherPath || !fs.existsSync(launcherPath)) {
|
|
201
|
+
return {
|
|
202
|
+
canCheck: true,
|
|
203
|
+
canSelfUpdate: false,
|
|
204
|
+
reason: 'The launcher entrypoint could not be resolved.',
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
canCheck: true,
|
|
209
|
+
canSelfUpdate: true,
|
|
210
|
+
reason: null,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function resolveNpmBinary() {
|
|
215
|
+
return resolveExecutableOnPath(process.platform === 'win32' ? 'npm.cmd' : 'npm') || resolveExecutableOnPath('npm');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function resolveLauncherPath() {
|
|
219
|
+
const configured = String(process.env.DEEPSCIENTIST_LAUNCHER_PATH || '').trim();
|
|
220
|
+
if (configured && fs.existsSync(configured)) {
|
|
221
|
+
return configured;
|
|
222
|
+
}
|
|
223
|
+
const candidate = path.join(repoRoot, 'bin', 'ds.js');
|
|
224
|
+
return fs.existsSync(candidate) ? candidate : null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function fetchLatestPublishedVersion({ npmBinary, timeoutMs = 3500 }) {
|
|
228
|
+
if (!npmBinary) {
|
|
229
|
+
return {
|
|
230
|
+
ok: false,
|
|
231
|
+
error: '`npm` is not available on PATH.',
|
|
232
|
+
latestVersion: null,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const result = spawnSync(npmBinary, ['view', UPDATE_PACKAGE_NAME, 'version', '--json'], {
|
|
236
|
+
encoding: 'utf8',
|
|
237
|
+
env: process.env,
|
|
238
|
+
timeout: timeoutMs,
|
|
239
|
+
});
|
|
240
|
+
if (result.error) {
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
error: result.error.message,
|
|
244
|
+
latestVersion: null,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (result.status !== 0) {
|
|
248
|
+
return {
|
|
249
|
+
ok: false,
|
|
250
|
+
error: (result.stderr || result.stdout || '').trim() || `npm exited with status ${result.status}`,
|
|
251
|
+
latestVersion: null,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
const parsed = JSON.parse(String(result.stdout || 'null'));
|
|
256
|
+
const latestVersion = Array.isArray(parsed) ? normalizeVersion(parsed[parsed.length - 1]) : normalizeVersion(parsed);
|
|
257
|
+
if (!latestVersion) {
|
|
258
|
+
throw new Error('npm returned an empty version string.');
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
ok: true,
|
|
262
|
+
error: null,
|
|
263
|
+
latestVersion,
|
|
264
|
+
};
|
|
265
|
+
} catch (error) {
|
|
266
|
+
return {
|
|
267
|
+
ok: false,
|
|
268
|
+
error: error instanceof Error ? error.message : 'Could not parse npm version output.',
|
|
269
|
+
latestVersion: null,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function buildUpdateStatus(home, statePatch = {}) {
|
|
275
|
+
const state = { ...readUpdateState(home), ...statePatch };
|
|
276
|
+
const installMode = detectInstallMode(repoRoot);
|
|
277
|
+
const npmBinary = resolveNpmBinary();
|
|
278
|
+
const launcherPath = resolveLauncherPath();
|
|
279
|
+
const support = updateSupportSummary(installMode, npmBinary, launcherPath);
|
|
280
|
+
const currentVersion = normalizeVersion(state.current_version || packageJson.version);
|
|
281
|
+
const latestVersion = normalizeVersion(state.latest_version || '');
|
|
282
|
+
const updateAvailable = Boolean(latestVersion) && compareVersions(latestVersion, currentVersion) > 0;
|
|
283
|
+
const skippedVersion = normalizeVersion(state.last_skipped_version || '');
|
|
284
|
+
const skippedCurrentTarget = Boolean(updateAvailable && skippedVersion && skippedVersion === latestVersion);
|
|
285
|
+
const promptRecommended =
|
|
286
|
+
Boolean(updateAvailable)
|
|
287
|
+
&& !Boolean(state.busy)
|
|
288
|
+
&& !skippedCurrentTarget
|
|
289
|
+
&& isExpired(state.last_prompted_at || state.last_deferred_at, UPDATE_PROMPT_TTL_MS);
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
ok: true,
|
|
293
|
+
package_name: UPDATE_PACKAGE_NAME,
|
|
294
|
+
install_mode: installMode,
|
|
295
|
+
can_check: support.canCheck,
|
|
296
|
+
can_self_update: support.canSelfUpdate,
|
|
297
|
+
current_version: currentVersion,
|
|
298
|
+
latest_version: latestVersion || null,
|
|
299
|
+
update_available: updateAvailable,
|
|
300
|
+
prompt_recommended: promptRecommended,
|
|
301
|
+
busy: Boolean(state.busy),
|
|
302
|
+
last_checked_at: state.last_checked_at || null,
|
|
303
|
+
last_check_error: state.last_check_error || null,
|
|
304
|
+
last_prompted_at: state.last_prompted_at || null,
|
|
305
|
+
last_deferred_at: state.last_deferred_at || null,
|
|
306
|
+
last_skipped_version: skippedVersion || null,
|
|
307
|
+
last_update_started_at: state.last_update_started_at || null,
|
|
308
|
+
last_update_finished_at: state.last_update_finished_at || null,
|
|
309
|
+
last_update_result: state.last_update_result || null,
|
|
310
|
+
target_version: normalizeVersion(state.target_version || '') || null,
|
|
311
|
+
manual_update_command: updateManualCommand(installMode),
|
|
312
|
+
reason: support.reason,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function checkForUpdates(home, { force = false, timeoutMs = 3500 } = {}) {
|
|
317
|
+
const currentVersion = normalizeVersion(packageJson.version);
|
|
318
|
+
const existing = readUpdateState(home);
|
|
319
|
+
const installMode = detectInstallMode(repoRoot);
|
|
320
|
+
const npmBinary = resolveNpmBinary();
|
|
321
|
+
const launcherPath = resolveLauncherPath();
|
|
322
|
+
const support = updateSupportSummary(installMode, npmBinary, launcherPath);
|
|
323
|
+
|
|
324
|
+
if (!force && existing.current_version === currentVersion && !isExpired(existing.last_checked_at, UPDATE_CHECK_TTL_MS)) {
|
|
325
|
+
return buildUpdateStatus(home);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (!support.canCheck) {
|
|
329
|
+
const patched = mergeUpdateState(home, {
|
|
330
|
+
current_version: currentVersion,
|
|
331
|
+
last_checked_at: new Date().toISOString(),
|
|
332
|
+
last_check_error: support.reason,
|
|
333
|
+
});
|
|
334
|
+
return buildUpdateStatus(home, patched);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const probe = fetchLatestPublishedVersion({ npmBinary, timeoutMs });
|
|
338
|
+
const patched = mergeUpdateState(home, {
|
|
339
|
+
current_version: currentVersion,
|
|
340
|
+
latest_version: probe.latestVersion || existing.latest_version || null,
|
|
341
|
+
last_checked_at: new Date().toISOString(),
|
|
342
|
+
last_check_error: probe.ok ? null : probe.error,
|
|
343
|
+
});
|
|
344
|
+
return buildUpdateStatus(home, patched);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function markUpdateDeferred(home, version) {
|
|
348
|
+
const patched = mergeUpdateState(home, {
|
|
349
|
+
last_prompted_at: new Date().toISOString(),
|
|
350
|
+
last_deferred_at: new Date().toISOString(),
|
|
351
|
+
latest_version: normalizeVersion(version || readUpdateState(home).latest_version || '') || null,
|
|
352
|
+
});
|
|
353
|
+
return buildUpdateStatus(home, patched);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function markUpdateSkipped(home, version) {
|
|
357
|
+
const normalized = normalizeVersion(version);
|
|
358
|
+
const patched = mergeUpdateState(home, {
|
|
359
|
+
last_prompted_at: new Date().toISOString(),
|
|
360
|
+
last_skipped_version: normalized || null,
|
|
361
|
+
});
|
|
362
|
+
return buildUpdateStatus(home, patched);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function parseRequiredPythonSpec(pyprojectText) {
|
|
366
|
+
const match = String(pyprojectText || '').match(/^\s*requires-python\s*=\s*["']([^"']+)["']/m);
|
|
367
|
+
return match ? match[1].trim() : '>=3.11';
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function parseMinimumPythonVersion(spec) {
|
|
371
|
+
const match = String(spec || '').match(/>=\s*(\d+)\.(\d+)(?:\.(\d+))?/);
|
|
372
|
+
if (!match) {
|
|
373
|
+
return { major: 3, minor: 11, patch: 0 };
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
major: Number(match[1]),
|
|
377
|
+
minor: Number(match[2]),
|
|
378
|
+
patch: Number(match[3] || 0),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
55
382
|
function resolveHome(args) {
|
|
56
383
|
const index = args.indexOf('--home');
|
|
57
384
|
if (index >= 0 && index + 1 < args.length) {
|
|
@@ -60,6 +387,9 @@ function resolveHome(args) {
|
|
|
60
387
|
if (process.env.DEEPSCIENTIST_HOME) {
|
|
61
388
|
return path.resolve(process.env.DEEPSCIENTIST_HOME);
|
|
62
389
|
}
|
|
390
|
+
if (process.env.DS_HOME) {
|
|
391
|
+
return path.resolve(process.env.DS_HOME);
|
|
392
|
+
}
|
|
63
393
|
return path.join(os.homedir(), 'DeepScientist');
|
|
64
394
|
}
|
|
65
395
|
|
|
@@ -88,6 +418,222 @@ function bindUiUrl(host, port) {
|
|
|
88
418
|
return `http://${formatHttpHost(normalized)}:${port}`;
|
|
89
419
|
}
|
|
90
420
|
|
|
421
|
+
function normalizeMode(value) {
|
|
422
|
+
const normalized = String(value || '')
|
|
423
|
+
.trim()
|
|
424
|
+
.toLowerCase();
|
|
425
|
+
if (normalized === 'tui' || normalized === 'both' || normalized === 'web') {
|
|
426
|
+
return normalized;
|
|
427
|
+
}
|
|
428
|
+
return 'web';
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function parseBooleanSetting(rawValue, fallback = false) {
|
|
432
|
+
if (typeof rawValue === 'boolean') {
|
|
433
|
+
return rawValue;
|
|
434
|
+
}
|
|
435
|
+
const normalized = String(rawValue || '')
|
|
436
|
+
.trim()
|
|
437
|
+
.toLowerCase();
|
|
438
|
+
if (['true', 'yes', 'on', '1'].includes(normalized)) {
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
if (['false', 'no', 'off', '0'].includes(normalized)) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
return fallback;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function supportsAnsi() {
|
|
448
|
+
return Boolean(process.stdout.isTTY && process.env.TERM !== 'dumb');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function stripAnsi(text) {
|
|
452
|
+
return String(text || '')
|
|
453
|
+
.replace(/\u001B]8;;[^\u0007]*\u0007/g, '')
|
|
454
|
+
.replace(/\u001B]8;;\u0007/g, '')
|
|
455
|
+
.replace(/\u001B\[[0-9;]*m/g, '');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function visibleWidth(text) {
|
|
459
|
+
return stripAnsi(text).length;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function centerText(text, width) {
|
|
463
|
+
const targetWidth = Math.max(visibleWidth(text), width || 0);
|
|
464
|
+
const padding = Math.max(0, Math.floor((targetWidth - visibleWidth(text)) / 2));
|
|
465
|
+
return `${' '.repeat(padding)}${text}`;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function hyperlink(url, label = url) {
|
|
469
|
+
if (!supportsAnsi()) {
|
|
470
|
+
return label;
|
|
471
|
+
}
|
|
472
|
+
return `\u001B]8;;${url}\u0007${label}\u001B]8;;\u0007`;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function colorize(code, text) {
|
|
476
|
+
if (!supportsAnsi()) {
|
|
477
|
+
return text;
|
|
478
|
+
}
|
|
479
|
+
return `${code}${text}\u001B[0m`;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function renderBrandArtwork() {
|
|
483
|
+
const brandPath = path.join(repoRoot, 'assets', 'branding', 'deepscientist-mark.png');
|
|
484
|
+
const chafa = resolveExecutableOnPath('chafa');
|
|
485
|
+
if (!supportsAnsi() || !chafa || !fs.existsSync(brandPath)) {
|
|
486
|
+
return [];
|
|
487
|
+
}
|
|
488
|
+
const width = Math.max(18, Math.min(30, Math.floor((process.stdout.columns || 100) / 3)));
|
|
489
|
+
const height = Math.max(8, Math.floor(width / 2));
|
|
490
|
+
try {
|
|
491
|
+
const result = spawnSync(
|
|
492
|
+
chafa,
|
|
493
|
+
['--size', `${width}x${height}`, '--format', 'symbols', '--colors', '16', brandPath],
|
|
494
|
+
{ encoding: 'utf8' }
|
|
495
|
+
);
|
|
496
|
+
if (result.status === 0 && result.stdout && result.stdout.trim()) {
|
|
497
|
+
return result.stdout.replace(/\s+$/, '').split(/\r?\n/);
|
|
498
|
+
}
|
|
499
|
+
} catch {}
|
|
500
|
+
return [];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function truncateMiddle(text, maxLength = 120) {
|
|
504
|
+
const value = String(text || '');
|
|
505
|
+
if (value.length <= maxLength) {
|
|
506
|
+
return value;
|
|
507
|
+
}
|
|
508
|
+
const head = Math.max(24, Math.floor((maxLength - 1) / 2));
|
|
509
|
+
const tail = Math.max(16, maxLength - head - 1);
|
|
510
|
+
return `${value.slice(0, head)}…${value.slice(-tail)}`;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function renderKeyValueRows(rows) {
|
|
514
|
+
const labelWidth = Math.max(...rows.map(([label]) => String(label).length), 8);
|
|
515
|
+
for (const [label, value] of rows) {
|
|
516
|
+
console.log(` ${String(label).padEnd(labelWidth)} ${value}`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function pythonMajorMinor(probe) {
|
|
521
|
+
if (!probe || typeof probe.major !== 'number' || typeof probe.minor !== 'number') {
|
|
522
|
+
return '';
|
|
523
|
+
}
|
|
524
|
+
return `${probe.major}.${probe.minor}`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function pythonVersionText(probe) {
|
|
528
|
+
if (!probe) {
|
|
529
|
+
return 'unknown';
|
|
530
|
+
}
|
|
531
|
+
const version = probe.version || pythonMajorMinor(probe) || 'unknown';
|
|
532
|
+
if (probe.executable) {
|
|
533
|
+
return `${version} (${probe.executable})`;
|
|
534
|
+
}
|
|
535
|
+
return version;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function renderLaunchHints({ home, url, bindUrl, pythonSelection }) {
|
|
539
|
+
const runtimeRows = [
|
|
540
|
+
['Version', packageJson.version],
|
|
541
|
+
['Home', truncateMiddle(home)],
|
|
542
|
+
['Browser URL', url],
|
|
543
|
+
['Bind URL', bindUrl],
|
|
544
|
+
['Python', truncateMiddle(pythonVersionText(pythonSelection))],
|
|
545
|
+
];
|
|
546
|
+
if (pythonSelection && pythonSelection.sourceLabel) {
|
|
547
|
+
runtimeRows.push(['Python source', pythonSelection.sourceLabel]);
|
|
548
|
+
}
|
|
549
|
+
console.log(colorize('\u001B[1;38;5;39m', 'Runtime'));
|
|
550
|
+
renderKeyValueRows(runtimeRows);
|
|
551
|
+
console.log('');
|
|
552
|
+
|
|
553
|
+
console.log(colorize('\u001B[1;38;5;39m', 'Quick Flags'));
|
|
554
|
+
renderKeyValueRows([
|
|
555
|
+
['ds --port 21000', 'Change the web port'],
|
|
556
|
+
['ds --host 0.0.0.0 --port 21000', 'Bind on all interfaces'],
|
|
557
|
+
['ds --both', 'Start web + TUI together'],
|
|
558
|
+
['ds --tui', 'Start the terminal workspace only'],
|
|
559
|
+
['ds --no-browser', 'Do not auto-open the browser'],
|
|
560
|
+
['ds --status', 'Show daemon health as JSON'],
|
|
561
|
+
['ds --restart', 'Restart the managed daemon'],
|
|
562
|
+
['ds --stop', 'Stop the managed daemon'],
|
|
563
|
+
['ds --help', 'Show the full launcher help'],
|
|
564
|
+
]);
|
|
565
|
+
console.log('');
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function printLaunchCard({
|
|
569
|
+
url,
|
|
570
|
+
bindUrl,
|
|
571
|
+
mode,
|
|
572
|
+
autoOpenRequested,
|
|
573
|
+
browserOpened,
|
|
574
|
+
daemonOnly,
|
|
575
|
+
home,
|
|
576
|
+
pythonSelection,
|
|
577
|
+
}) {
|
|
578
|
+
const width = Math.max(72, Math.min(process.stdout.columns || 100, 108));
|
|
579
|
+
const divider = colorize('\u001B[38;5;245m', '─'.repeat(Math.max(36, width - 6)));
|
|
580
|
+
const title = colorize('\u001B[1;38;5;39m', 'ResearAI');
|
|
581
|
+
const subtitle = colorize('\u001B[38;5;110m', 'Local-first research operating system');
|
|
582
|
+
const versionLine = colorize('\u001B[38;5;245m', `Version ${packageJson.version}`);
|
|
583
|
+
const urlLabel = colorize('\u001B[1;38;5;45m', hyperlink(url, url));
|
|
584
|
+
const workspaceMode =
|
|
585
|
+
mode === 'both'
|
|
586
|
+
? 'Web workspace + terminal workspace'
|
|
587
|
+
: mode === 'tui'
|
|
588
|
+
? 'Terminal workspace'
|
|
589
|
+
: 'Web workspace';
|
|
590
|
+
const browserLine = autoOpenRequested
|
|
591
|
+
? browserOpened
|
|
592
|
+
? 'Browser launch requested successfully.'
|
|
593
|
+
: 'Browser auto-open was requested but is not available in this terminal session.'
|
|
594
|
+
: 'Browser auto-open is disabled. Open the URL manually if needed.';
|
|
595
|
+
const nextStep = daemonOnly
|
|
596
|
+
? 'Use ds --tui to enter the terminal workspace.'
|
|
597
|
+
: mode === 'web'
|
|
598
|
+
? 'Use ds --tui to enter the terminal workspace.'
|
|
599
|
+
: mode === 'both'
|
|
600
|
+
? 'The terminal workspace starts below.'
|
|
601
|
+
: 'Use Ctrl+O inside TUI to reopen the web workspace.';
|
|
602
|
+
|
|
603
|
+
console.log('');
|
|
604
|
+
const artwork = renderBrandArtwork();
|
|
605
|
+
for (const line of artwork) {
|
|
606
|
+
console.log(centerText(line, width));
|
|
607
|
+
}
|
|
608
|
+
if (artwork.length === 0) {
|
|
609
|
+
console.log(centerText(colorize('\u001B[1;38;5;39m', '⛰'), width));
|
|
610
|
+
}
|
|
611
|
+
const wordmark = [
|
|
612
|
+
' ____ ____ _ _ _ _ ',
|
|
613
|
+
' | _ \\ ___ ___ _ __ / ___| ___(_) ___ _ __ | |_(_)___| |_ ',
|
|
614
|
+
" | | | |/ _ \\/ _ \\ '_ \\\\___ \\ / __| |/ _ \\ '_ \\| __| / __| __|",
|
|
615
|
+
' | |_| | __/ __/ |_) |___) | (__| | __/ | | | |_| \\__ \\ |_ ',
|
|
616
|
+
' |____/ \\___|\\___| .__/|____/ \\___|_|\\___|_| |_|\\__|_|___/\\__|',
|
|
617
|
+
' |_| ',
|
|
618
|
+
];
|
|
619
|
+
console.log(centerText(title, width));
|
|
620
|
+
console.log(centerText(versionLine, width));
|
|
621
|
+
for (const line of wordmark) {
|
|
622
|
+
console.log(centerText(colorize('\u001B[1;38;5;39m', line), width));
|
|
623
|
+
}
|
|
624
|
+
console.log(centerText(subtitle, width));
|
|
625
|
+
console.log('');
|
|
626
|
+
console.log(centerText(divider, width));
|
|
627
|
+
console.log(centerText(colorize('\u001B[1m', workspaceMode), width));
|
|
628
|
+
console.log(centerText(urlLabel, width));
|
|
629
|
+
console.log(centerText(divider, width));
|
|
630
|
+
console.log(centerText(browserLine, width));
|
|
631
|
+
console.log(centerText(nextStep, width));
|
|
632
|
+
console.log(centerText('Run ds --stop to stop the managed daemon.', width));
|
|
633
|
+
console.log('');
|
|
634
|
+
renderLaunchHints({ home, url, bindUrl, pythonSelection });
|
|
635
|
+
}
|
|
636
|
+
|
|
91
637
|
function escapeHtml(value) {
|
|
92
638
|
return String(value ?? '')
|
|
93
639
|
.replaceAll('&', '&')
|
|
@@ -208,7 +754,7 @@ function writeCodexPreflightReport(home, probe) {
|
|
|
208
754
|
};
|
|
209
755
|
}
|
|
210
756
|
|
|
211
|
-
function readCodexBootstrapState(home,
|
|
757
|
+
function readCodexBootstrapState(home, runtimePython) {
|
|
212
758
|
const snippet = [
|
|
213
759
|
'import json, pathlib, sys',
|
|
214
760
|
'from deepscientist.config import ConfigManager',
|
|
@@ -216,7 +762,7 @@ function readCodexBootstrapState(home, venvPython) {
|
|
|
216
762
|
'manager = ConfigManager(home)',
|
|
217
763
|
'print(json.dumps(manager.codex_bootstrap_state(), ensure_ascii=False))',
|
|
218
764
|
].join('\n');
|
|
219
|
-
const result = runSync(
|
|
765
|
+
const result = runSync(runtimePython, ['-c', snippet, home], { capture: true, allowFailure: true });
|
|
220
766
|
if (result.status !== 0) {
|
|
221
767
|
return { codex_ready: false, codex_last_checked_at: null, codex_last_result: {} };
|
|
222
768
|
}
|
|
@@ -227,7 +773,7 @@ function readCodexBootstrapState(home, venvPython) {
|
|
|
227
773
|
}
|
|
228
774
|
}
|
|
229
775
|
|
|
230
|
-
function probeCodexBootstrap(home,
|
|
776
|
+
function probeCodexBootstrap(home, runtimePython) {
|
|
231
777
|
const snippet = [
|
|
232
778
|
'import json, pathlib, sys',
|
|
233
779
|
'from deepscientist.config import ConfigManager',
|
|
@@ -235,7 +781,7 @@ function probeCodexBootstrap(home, venvPython) {
|
|
|
235
781
|
'manager = ConfigManager(home)',
|
|
236
782
|
'print(json.dumps(manager.probe_codex_bootstrap(persist=True), ensure_ascii=False))',
|
|
237
783
|
].join('\n');
|
|
238
|
-
const result = runSync(
|
|
784
|
+
const result = runSync(runtimePython, ['-c', snippet, home], { capture: true, allowFailure: true });
|
|
239
785
|
let payload = null;
|
|
240
786
|
try {
|
|
241
787
|
payload = JSON.parse(result.stdout || '{}');
|
|
@@ -274,16 +820,17 @@ function createCodexPreflightError(home, probe) {
|
|
|
274
820
|
|
|
275
821
|
function parseLauncherArgs(argv) {
|
|
276
822
|
const args = [...argv];
|
|
277
|
-
let mode =
|
|
823
|
+
let mode = null;
|
|
278
824
|
let host = null;
|
|
279
825
|
let port = null;
|
|
280
826
|
let home = null;
|
|
281
827
|
let stop = false;
|
|
282
828
|
let restart = false;
|
|
283
|
-
let openBrowser =
|
|
829
|
+
let openBrowser = null;
|
|
284
830
|
let questId = null;
|
|
285
831
|
let status = false;
|
|
286
832
|
let daemonOnly = false;
|
|
833
|
+
let skipUpdateCheck = false;
|
|
287
834
|
|
|
288
835
|
if (args[0] === 'ui') {
|
|
289
836
|
args.shift();
|
|
@@ -301,11 +848,12 @@ function parseLauncherArgs(argv) {
|
|
|
301
848
|
else if (arg === '--no-browser') openBrowser = false;
|
|
302
849
|
else if (arg === '--open-browser') openBrowser = true;
|
|
303
850
|
else if (arg === '--daemon-only') daemonOnly = true;
|
|
851
|
+
else if (arg === '--skip-update-check') skipUpdateCheck = true;
|
|
304
852
|
else if (arg === '--host' && args[index + 1]) host = args[++index];
|
|
305
853
|
else if (arg === '--port' && args[index + 1]) port = Number(args[++index]);
|
|
306
854
|
else if (arg === '--home' && args[index + 1]) home = path.resolve(args[++index]);
|
|
307
855
|
else if (arg === '--quest-id' && args[index + 1]) questId = args[++index];
|
|
308
|
-
else if (arg === '--mode' && args[index + 1]) mode = args[++index];
|
|
856
|
+
else if (arg === '--mode' && args[index + 1]) mode = normalizeMode(args[++index]);
|
|
309
857
|
else if (arg === '--help' || arg === '-h') return { help: true };
|
|
310
858
|
else if (!arg.startsWith('--')) return null;
|
|
311
859
|
}
|
|
@@ -322,6 +870,83 @@ function parseLauncherArgs(argv) {
|
|
|
322
870
|
openBrowser,
|
|
323
871
|
questId,
|
|
324
872
|
daemonOnly,
|
|
873
|
+
skipUpdateCheck,
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function printUpdateHelp() {
|
|
878
|
+
console.log(`DeepScientist update
|
|
879
|
+
|
|
880
|
+
Usage:
|
|
881
|
+
ds update
|
|
882
|
+
ds update --check
|
|
883
|
+
ds update --yes
|
|
884
|
+
ds update --remind-later
|
|
885
|
+
ds update --skip-version
|
|
886
|
+
|
|
887
|
+
Flags:
|
|
888
|
+
--check Return the current update status without installing
|
|
889
|
+
--yes Install the latest published npm package immediately
|
|
890
|
+
--json Print structured JSON output
|
|
891
|
+
--force-check Ignore the cached version probe
|
|
892
|
+
--remind-later Defer prompts for the current published version
|
|
893
|
+
--skip-version Skip reminders for the current published version
|
|
894
|
+
`);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function parseUpdateArgs(argv) {
|
|
898
|
+
const args = [...argv];
|
|
899
|
+
if (args[0] === 'update') {
|
|
900
|
+
args.shift();
|
|
901
|
+
}
|
|
902
|
+
let json = false;
|
|
903
|
+
let check = false;
|
|
904
|
+
let yes = false;
|
|
905
|
+
let forceCheck = false;
|
|
906
|
+
let remindLater = false;
|
|
907
|
+
let skipVersion = false;
|
|
908
|
+
let background = false;
|
|
909
|
+
let worker = false;
|
|
910
|
+
let home = null;
|
|
911
|
+
let host = null;
|
|
912
|
+
let port = null;
|
|
913
|
+
let restartDaemon = null;
|
|
914
|
+
let skipUpdateCheck = false;
|
|
915
|
+
|
|
916
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
917
|
+
const arg = args[index];
|
|
918
|
+
if (arg === '--json') json = true;
|
|
919
|
+
else if (arg === '--check') check = true;
|
|
920
|
+
else if (arg === '--yes') yes = true;
|
|
921
|
+
else if (arg === '--force-check') forceCheck = true;
|
|
922
|
+
else if (arg === '--remind-later') remindLater = true;
|
|
923
|
+
else if (arg === '--skip-version') skipVersion = true;
|
|
924
|
+
else if (arg === '--background') background = true;
|
|
925
|
+
else if (arg === '--worker') worker = true;
|
|
926
|
+
else if (arg === '--restart-daemon') restartDaemon = true;
|
|
927
|
+
else if (arg === '--skip-update-check') skipUpdateCheck = true;
|
|
928
|
+
else if (arg === '--home' && args[index + 1]) home = path.resolve(args[++index]);
|
|
929
|
+
else if (arg === '--host' && args[index + 1]) host = args[++index];
|
|
930
|
+
else if (arg === '--port' && args[index + 1]) port = Number(args[++index]);
|
|
931
|
+
else if (arg === '--help' || arg === '-h') return { help: true };
|
|
932
|
+
else if (!arg.startsWith('--')) return null;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
return {
|
|
936
|
+
help: false,
|
|
937
|
+
json,
|
|
938
|
+
check,
|
|
939
|
+
yes,
|
|
940
|
+
forceCheck,
|
|
941
|
+
remindLater,
|
|
942
|
+
skipVersion,
|
|
943
|
+
background,
|
|
944
|
+
worker,
|
|
945
|
+
home,
|
|
946
|
+
host,
|
|
947
|
+
port,
|
|
948
|
+
restartDaemon,
|
|
949
|
+
skipUpdateCheck,
|
|
325
950
|
};
|
|
326
951
|
}
|
|
327
952
|
|
|
@@ -340,39 +965,268 @@ function findFirstPositionalArg(args) {
|
|
|
340
965
|
return null;
|
|
341
966
|
}
|
|
342
967
|
|
|
343
|
-
function
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
968
|
+
function realpathOrSelf(targetPath) {
|
|
969
|
+
try {
|
|
970
|
+
return fs.realpathSync(targetPath);
|
|
971
|
+
} catch {
|
|
972
|
+
return targetPath;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function pythonMeetsMinimum(probe) {
|
|
977
|
+
if (!probe || typeof probe.major !== 'number' || typeof probe.minor !== 'number') {
|
|
978
|
+
return false;
|
|
979
|
+
}
|
|
980
|
+
if (probe.major !== minimumPythonVersion.major) {
|
|
981
|
+
return probe.major > minimumPythonVersion.major;
|
|
982
|
+
}
|
|
983
|
+
if (probe.minor !== minimumPythonVersion.minor) {
|
|
984
|
+
return probe.minor > minimumPythonVersion.minor;
|
|
985
|
+
}
|
|
986
|
+
return probe.patch >= minimumPythonVersion.patch;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function pythonSelectionLabel(source) {
|
|
990
|
+
if (source === 'conda') {
|
|
991
|
+
const envName = String(process.env.CONDA_DEFAULT_ENV || '').trim();
|
|
992
|
+
return envName ? `conda:${envName}` : 'conda';
|
|
993
|
+
}
|
|
994
|
+
if (source === 'uv-managed') {
|
|
995
|
+
return 'uv-managed';
|
|
996
|
+
}
|
|
997
|
+
return 'path';
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function buildCondaPythonCandidates() {
|
|
1001
|
+
const prefix = String(process.env.CONDA_PREFIX || '').trim();
|
|
1002
|
+
if (!prefix) {
|
|
1003
|
+
return [];
|
|
1004
|
+
}
|
|
1005
|
+
if (process.platform === 'win32') {
|
|
1006
|
+
return [path.join(prefix, 'python.exe'), path.join(prefix, 'Scripts', 'python.exe')];
|
|
1007
|
+
}
|
|
1008
|
+
return [path.join(prefix, 'bin', 'python'), path.join(prefix, 'bin', 'python3')];
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function probePython(binary) {
|
|
1012
|
+
const snippet = [
|
|
1013
|
+
'import json, sys',
|
|
1014
|
+
'print(json.dumps({',
|
|
1015
|
+
' "executable": sys.executable,',
|
|
1016
|
+
' "version": ".".join(str(part) for part in sys.version_info[:3]),',
|
|
1017
|
+
' "major": sys.version_info[0],',
|
|
1018
|
+
' "minor": sys.version_info[1],',
|
|
1019
|
+
' "patch": sys.version_info[2],',
|
|
1020
|
+
'}, ensure_ascii=False))',
|
|
1021
|
+
].join('\n');
|
|
1022
|
+
const result = spawnSync(binary, ['-c', snippet], {
|
|
1023
|
+
encoding: 'utf8',
|
|
1024
|
+
env: process.env,
|
|
1025
|
+
});
|
|
1026
|
+
if (result.error) {
|
|
1027
|
+
return {
|
|
1028
|
+
ok: false,
|
|
1029
|
+
binary,
|
|
1030
|
+
error: result.error.message,
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
if (result.status !== 0) {
|
|
1034
|
+
return {
|
|
1035
|
+
ok: false,
|
|
1036
|
+
binary,
|
|
1037
|
+
error: (result.stderr || result.stdout || '').trim() || `exit ${result.status}`,
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
try {
|
|
1041
|
+
const payload = JSON.parse(result.stdout || '{}');
|
|
1042
|
+
const executable = String(payload.executable || '').trim();
|
|
1043
|
+
return {
|
|
1044
|
+
ok: true,
|
|
1045
|
+
binary,
|
|
1046
|
+
executable,
|
|
1047
|
+
realExecutable: executable ? realpathOrSelf(executable) : '',
|
|
1048
|
+
version: String(payload.version || '').trim(),
|
|
1049
|
+
major: Number(payload.major),
|
|
1050
|
+
minor: Number(payload.minor),
|
|
1051
|
+
patch: Number(payload.patch),
|
|
1052
|
+
};
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
return {
|
|
1055
|
+
ok: false,
|
|
1056
|
+
binary,
|
|
1057
|
+
error: error instanceof Error ? error.message : 'Could not parse Python version probe.',
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function minimumPythonRequest() {
|
|
1063
|
+
return `${minimumPythonVersion.major}.${minimumPythonVersion.minor}`;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function decoratePythonProbe(probe, source) {
|
|
1067
|
+
if (!probe || !probe.ok) {
|
|
1068
|
+
return null;
|
|
1069
|
+
}
|
|
1070
|
+
return {
|
|
1071
|
+
...probe,
|
|
1072
|
+
source,
|
|
1073
|
+
sourceLabel: pythonSelectionLabel(source),
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function collectPythonProbes(binaries, source, seenExecutables) {
|
|
1078
|
+
const probes = [];
|
|
1079
|
+
for (const candidate of binaries) {
|
|
1080
|
+
const resolved = decoratePythonProbe(probePython(candidate), source);
|
|
1081
|
+
if (!resolved) {
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
const executableKey = resolved.realExecutable || resolved.executable || resolved.binary;
|
|
1085
|
+
if (seenExecutables.has(executableKey)) {
|
|
1086
|
+
continue;
|
|
348
1087
|
}
|
|
1088
|
+
seenExecutables.add(executableKey);
|
|
1089
|
+
probes.push(resolved);
|
|
349
1090
|
}
|
|
350
|
-
|
|
351
|
-
|
|
1091
|
+
return probes;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function createPythonRuntimePlan({ condaProbes = [], pathProbes = [], minimumVersionRequest = minimumPythonRequest() }) {
|
|
1095
|
+
const validConda = condaProbes.find((probe) => pythonMeetsMinimum(probe)) || null;
|
|
1096
|
+
if (validConda) {
|
|
1097
|
+
return {
|
|
1098
|
+
runtimeKind: 'system',
|
|
1099
|
+
selectedProbe: validConda,
|
|
1100
|
+
source: 'conda',
|
|
1101
|
+
sourceLabel: validConda.sourceLabel,
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
const firstConda = condaProbes[0] || null;
|
|
1105
|
+
if (firstConda) {
|
|
1106
|
+
return {
|
|
1107
|
+
runtimeKind: 'managed',
|
|
1108
|
+
selectedProbe: null,
|
|
1109
|
+
rejectedProbe: firstConda,
|
|
1110
|
+
source: 'conda',
|
|
1111
|
+
sourceLabel: pythonSelectionLabel('uv-managed'),
|
|
1112
|
+
minimumVersionRequest,
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
const validPath = pathProbes.find((probe) => pythonMeetsMinimum(probe)) || null;
|
|
1117
|
+
if (validPath) {
|
|
1118
|
+
return {
|
|
1119
|
+
runtimeKind: 'system',
|
|
1120
|
+
selectedProbe: validPath,
|
|
1121
|
+
source: 'path',
|
|
1122
|
+
sourceLabel: validPath.sourceLabel,
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
const firstPath = pathProbes[0] || null;
|
|
1126
|
+
if (firstPath) {
|
|
1127
|
+
return {
|
|
1128
|
+
runtimeKind: 'managed',
|
|
1129
|
+
selectedProbe: null,
|
|
1130
|
+
rejectedProbe: firstPath,
|
|
1131
|
+
source: 'path',
|
|
1132
|
+
sourceLabel: pythonSelectionLabel('uv-managed'),
|
|
1133
|
+
minimumVersionRequest,
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
return {
|
|
1138
|
+
runtimeKind: 'managed',
|
|
1139
|
+
selectedProbe: null,
|
|
1140
|
+
rejectedProbe: null,
|
|
1141
|
+
source: 'uv-managed',
|
|
1142
|
+
sourceLabel: pythonSelectionLabel('uv-managed'),
|
|
1143
|
+
minimumVersionRequest,
|
|
1144
|
+
};
|
|
352
1145
|
}
|
|
353
1146
|
|
|
354
|
-
function
|
|
1147
|
+
function printManagedPythonFallbackNotice({ rejectedProbe, source, minimumVersionRequest, installDir }) {
|
|
1148
|
+
if (!rejectedProbe) {
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
const envName = String(process.env.CONDA_DEFAULT_ENV || '').trim();
|
|
1152
|
+
const sourceLabel =
|
|
1153
|
+
source === 'conda'
|
|
1154
|
+
? (envName ? `active conda environment \`${envName}\`` : 'active conda environment')
|
|
1155
|
+
: 'detected system Python';
|
|
1156
|
+
console.warn('');
|
|
1157
|
+
console.warn(
|
|
1158
|
+
`DeepScientist found ${sourceLabel} at ${pythonVersionText(rejectedProbe)}, which does not satisfy Python ${requiredPythonSpec}.`
|
|
1159
|
+
);
|
|
1160
|
+
console.warn(
|
|
1161
|
+
`DeepScientist will provision a uv-managed Python ${minimumVersionRequest}+ runtime under ${installDir}.`
|
|
1162
|
+
);
|
|
1163
|
+
console.warn('');
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function resolvePythonRuntimePlan() {
|
|
1167
|
+
const seenExecutables = new Set();
|
|
1168
|
+
const condaProbes = collectPythonProbes(buildCondaPythonCandidates(), 'conda', seenExecutables);
|
|
1169
|
+
const pathProbes = collectPythonProbes(pythonCandidates, 'path', seenExecutables);
|
|
1170
|
+
return createPythonRuntimePlan({ condaProbes, pathProbes, minimumVersionRequest: minimumPythonRequest() });
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function runtimePythonEnvPath(home) {
|
|
1174
|
+
return path.join(home, 'runtime', 'python-env');
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function runtimePythonPath(home) {
|
|
355
1178
|
return process.platform === 'win32'
|
|
356
|
-
? path.join(home, '
|
|
357
|
-
: path.join(home, '
|
|
1179
|
+
? path.join(runtimePythonEnvPath(home), 'Scripts', 'python.exe')
|
|
1180
|
+
: path.join(runtimePythonEnvPath(home), 'bin', 'python');
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function runtimeUvCachePath(home) {
|
|
1184
|
+
return path.join(home, 'runtime', 'uv-cache');
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function runtimeUvPythonInstallPath(home) {
|
|
1188
|
+
return path.join(home, 'runtime', 'python');
|
|
358
1189
|
}
|
|
359
1190
|
|
|
360
|
-
function
|
|
1191
|
+
function runtimeToolsPath(home) {
|
|
1192
|
+
return path.join(home, 'runtime', 'tools');
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function runtimeUvRootPath(home) {
|
|
1196
|
+
return path.join(runtimeToolsPath(home), 'uv');
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function runtimeUvBinDir(home) {
|
|
1200
|
+
return path.join(runtimeUvRootPath(home), 'bin');
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function runtimeUvBinaryPath(home) {
|
|
1204
|
+
return path.join(runtimeUvBinDir(home), process.platform === 'win32' ? 'uv.exe' : 'uv');
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function legacyVenvRootPath(home) {
|
|
361
1208
|
return path.join(home, 'runtime', 'venv');
|
|
362
1209
|
}
|
|
363
1210
|
|
|
1211
|
+
function useEditableProjectInstall() {
|
|
1212
|
+
return fs.existsSync(path.join(repoRoot, '.git'));
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function uvLockPath() {
|
|
1216
|
+
return path.join(repoRoot, 'uv.lock');
|
|
1217
|
+
}
|
|
1218
|
+
|
|
364
1219
|
function sha256File(filePath) {
|
|
365
1220
|
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
|
366
1221
|
}
|
|
367
1222
|
|
|
368
|
-
function
|
|
369
|
-
const skillsRoot = path.join(repoRoot, 'src', 'skills');
|
|
1223
|
+
function hashDirectoryTree(rootPath, predicate = null) {
|
|
370
1224
|
const hasher = crypto.createHash('sha256');
|
|
371
|
-
if (!fs.existsSync(
|
|
1225
|
+
if (!fs.existsSync(rootPath)) {
|
|
372
1226
|
hasher.update('missing');
|
|
373
1227
|
return hasher.digest('hex');
|
|
374
1228
|
}
|
|
375
|
-
const stack = [
|
|
1229
|
+
const stack = [rootPath];
|
|
376
1230
|
const files = [];
|
|
377
1231
|
while (stack.length > 0) {
|
|
378
1232
|
const current = stack.pop();
|
|
@@ -383,18 +1237,29 @@ function hashSkillTree() {
|
|
|
383
1237
|
continue;
|
|
384
1238
|
}
|
|
385
1239
|
if (entry.isFile()) {
|
|
1240
|
+
if (typeof predicate === 'function' && !predicate(fullPath)) {
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
386
1243
|
files.push(fullPath);
|
|
387
1244
|
}
|
|
388
1245
|
}
|
|
389
1246
|
}
|
|
390
1247
|
files.sort();
|
|
391
1248
|
for (const filePath of files) {
|
|
392
|
-
hasher.update(path.relative(
|
|
1249
|
+
hasher.update(path.relative(rootPath, filePath));
|
|
393
1250
|
hasher.update(fs.readFileSync(filePath));
|
|
394
1251
|
}
|
|
395
1252
|
return hasher.digest('hex');
|
|
396
1253
|
}
|
|
397
1254
|
|
|
1255
|
+
function hashSkillTree() {
|
|
1256
|
+
return hashDirectoryTree(path.join(repoRoot, 'src', 'skills'));
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function hashPythonSourceTree() {
|
|
1260
|
+
return hashDirectoryTree(path.join(repoRoot, 'src', 'deepscientist'), (filePath) => filePath.endsWith('.py'));
|
|
1261
|
+
}
|
|
1262
|
+
|
|
398
1263
|
function discoverSkillIds() {
|
|
399
1264
|
const skillsRoot = path.join(repoRoot, 'src', 'skills');
|
|
400
1265
|
if (!fs.existsSync(skillsRoot)) {
|
|
@@ -425,15 +1290,11 @@ function globalSkillsInstalled() {
|
|
|
425
1290
|
|
|
426
1291
|
function runSync(binary, args, options = {}) {
|
|
427
1292
|
const result = spawnSync(binary, args, {
|
|
428
|
-
cwd: repoRoot,
|
|
1293
|
+
cwd: options.cwd || repoRoot,
|
|
429
1294
|
stdio: options.capture ? 'pipe' : 'inherit',
|
|
430
|
-
env:
|
|
431
|
-
...process.env,
|
|
432
|
-
PYTHONPATH: process.env.PYTHONPATH
|
|
433
|
-
? `${srcPath}${path.delimiter}${process.env.PYTHONPATH}`
|
|
434
|
-
: srcPath,
|
|
435
|
-
},
|
|
1295
|
+
env: options.env || process.env,
|
|
436
1296
|
encoding: 'utf8',
|
|
1297
|
+
input: options.input,
|
|
437
1298
|
});
|
|
438
1299
|
if (result.error) {
|
|
439
1300
|
throw result.error;
|
|
@@ -451,72 +1312,468 @@ function step(index, total, message) {
|
|
|
451
1312
|
console.log(`[${index}/${total}] ${message}`);
|
|
452
1313
|
}
|
|
453
1314
|
|
|
454
|
-
function
|
|
455
|
-
runSync(venvPython, ['-m', 'pip', 'install', '--upgrade', 'pip', 'setuptools', 'wheel']);
|
|
456
|
-
runSync(venvPython, ['-m', 'pip', 'install', '--upgrade', repoRoot]);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function verifyPythonRuntime(venvPython) {
|
|
1315
|
+
function verifyPythonRuntime(runtimePython) {
|
|
460
1316
|
const result = runSync(
|
|
461
|
-
|
|
1317
|
+
runtimePython,
|
|
462
1318
|
['-c', 'import deepscientist.cli; import cryptography; import _cffi_backend; print("ok")'],
|
|
463
1319
|
{ capture: true, allowFailure: true }
|
|
464
1320
|
);
|
|
465
1321
|
return result.status === 0;
|
|
466
1322
|
}
|
|
467
1323
|
|
|
468
|
-
function
|
|
469
|
-
fs.
|
|
470
|
-
|
|
471
|
-
|
|
1324
|
+
function readJsonFile(filePath) {
|
|
1325
|
+
if (!fs.existsSync(filePath)) {
|
|
1326
|
+
return null;
|
|
1327
|
+
}
|
|
1328
|
+
try {
|
|
1329
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
1330
|
+
} catch {
|
|
1331
|
+
return null;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function executableExtensions() {
|
|
1336
|
+
if (process.platform !== 'win32') {
|
|
1337
|
+
return [''];
|
|
1338
|
+
}
|
|
1339
|
+
return (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM')
|
|
1340
|
+
.split(';')
|
|
1341
|
+
.filter(Boolean);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function candidateExecutablePaths(basePath) {
|
|
1345
|
+
if (process.platform !== 'win32') {
|
|
1346
|
+
return [basePath];
|
|
1347
|
+
}
|
|
1348
|
+
const extension = path.extname(basePath);
|
|
1349
|
+
if (extension) {
|
|
1350
|
+
return [basePath];
|
|
1351
|
+
}
|
|
1352
|
+
return executableExtensions().map((suffix) => `${basePath}${suffix}`);
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function isExecutableFile(candidate) {
|
|
1356
|
+
try {
|
|
1357
|
+
if (!fs.existsSync(candidate)) {
|
|
1358
|
+
return false;
|
|
1359
|
+
}
|
|
1360
|
+
const stat = fs.statSync(candidate);
|
|
1361
|
+
if (!stat.isFile()) {
|
|
1362
|
+
return false;
|
|
1363
|
+
}
|
|
1364
|
+
if (process.platform !== 'win32') {
|
|
1365
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
1366
|
+
}
|
|
1367
|
+
return true;
|
|
1368
|
+
} catch {
|
|
1369
|
+
return false;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
function resolveBinaryReference(reference) {
|
|
1374
|
+
const normalized = String(reference || '').trim();
|
|
1375
|
+
if (!normalized) {
|
|
1376
|
+
return null;
|
|
1377
|
+
}
|
|
1378
|
+
const expanded = expandUserPath(normalized);
|
|
1379
|
+
if (
|
|
1380
|
+
path.isAbsolute(expanded)
|
|
1381
|
+
|| normalized.startsWith('.')
|
|
1382
|
+
|| normalized.includes(path.sep)
|
|
1383
|
+
|| (path.sep === '\\' ? normalized.includes('/') : normalized.includes('\\'))
|
|
1384
|
+
) {
|
|
1385
|
+
const absolute = path.isAbsolute(expanded) ? expanded : path.resolve(expanded);
|
|
1386
|
+
for (const candidate of candidateExecutablePaths(absolute)) {
|
|
1387
|
+
if (isExecutableFile(candidate)) {
|
|
1388
|
+
return candidate;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
return null;
|
|
1392
|
+
}
|
|
1393
|
+
return resolveExecutableOnPath(expanded);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function resolveUvBinary(home) {
|
|
1397
|
+
const configured = String(process.env.DEEPSCIENTIST_UV || process.env.UV_BIN || '').trim();
|
|
1398
|
+
if (configured) {
|
|
1399
|
+
return {
|
|
1400
|
+
path: resolveBinaryReference(configured),
|
|
1401
|
+
source: 'env',
|
|
1402
|
+
configured,
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
const local = resolveBinaryReference(runtimeUvBinaryPath(home));
|
|
1406
|
+
if (local) {
|
|
1407
|
+
return {
|
|
1408
|
+
path: local,
|
|
1409
|
+
source: 'local',
|
|
1410
|
+
configured: null,
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
const discovered = resolveExecutableOnPath('uv');
|
|
1414
|
+
return {
|
|
1415
|
+
path: discovered,
|
|
1416
|
+
source: discovered ? 'path' : null,
|
|
1417
|
+
configured: null,
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
function printUvInstallGuidance(home, errorMessage = null) {
|
|
1422
|
+
console.error('');
|
|
1423
|
+
if (errorMessage) {
|
|
1424
|
+
console.error(`DeepScientist could not prepare a local uv runtime manager: ${errorMessage}`);
|
|
1425
|
+
} else {
|
|
1426
|
+
console.error('DeepScientist could not find a usable uv runtime manager.');
|
|
1427
|
+
}
|
|
1428
|
+
console.error(`DeepScientist normally installs uv automatically under ${runtimeUvBinDir(home)}.`);
|
|
1429
|
+
console.error('If the automatic bootstrap fails, install uv manually and run `ds` again.');
|
|
1430
|
+
console.error('');
|
|
1431
|
+
if (process.platform === 'win32') {
|
|
1432
|
+
console.error('Windows PowerShell:');
|
|
1433
|
+
console.error(' powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"');
|
|
1434
|
+
} else {
|
|
1435
|
+
console.error('macOS / Linux:');
|
|
1436
|
+
console.error(' curl -LsSf https://astral.sh/uv/install.sh | sh');
|
|
1437
|
+
}
|
|
1438
|
+
console.error('Alternative:');
|
|
1439
|
+
console.error(' pipx install uv');
|
|
1440
|
+
console.error('');
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function downloadFileWithNode(url, destinationPath) {
|
|
1444
|
+
const downloader = [
|
|
1445
|
+
'const fs = require("node:fs");',
|
|
1446
|
+
'const url = process.argv[1];',
|
|
1447
|
+
'const destination = process.argv[2];',
|
|
1448
|
+
'const timeoutMs = Number(process.argv[3] || "45000");',
|
|
1449
|
+
'(async () => {',
|
|
1450
|
+
' const controller = new AbortController();',
|
|
1451
|
+
' const timer = setTimeout(() => controller.abort(), timeoutMs);',
|
|
1452
|
+
' try {',
|
|
1453
|
+
' const response = await fetch(url, { signal: controller.signal });',
|
|
1454
|
+
' if (!response.ok) {',
|
|
1455
|
+
' throw new Error(`HTTP ${response.status} ${response.statusText}`);',
|
|
1456
|
+
' }',
|
|
1457
|
+
' const body = await response.text();',
|
|
1458
|
+
' fs.writeFileSync(destination, body, "utf8");',
|
|
1459
|
+
' } finally {',
|
|
1460
|
+
' clearTimeout(timer);',
|
|
1461
|
+
' }',
|
|
1462
|
+
'})().catch((error) => {',
|
|
1463
|
+
' console.error(error instanceof Error ? error.message : String(error));',
|
|
1464
|
+
' process.exit(1);',
|
|
1465
|
+
'});',
|
|
1466
|
+
].join('\n');
|
|
1467
|
+
const result = spawnSync(process.execPath, ['-e', downloader, url, destinationPath, '45000'], {
|
|
1468
|
+
cwd: repoRoot,
|
|
1469
|
+
stdio: 'inherit',
|
|
1470
|
+
env: process.env,
|
|
1471
|
+
});
|
|
1472
|
+
if (result.error) {
|
|
1473
|
+
throw result.error;
|
|
1474
|
+
}
|
|
1475
|
+
if (result.status !== 0) {
|
|
1476
|
+
throw new Error(`Download failed with status ${result.status ?? 1}.`);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
function installLocalUv(home) {
|
|
1481
|
+
const uvRoot = runtimeUvRootPath(home);
|
|
1482
|
+
const binDir = runtimeUvBinDir(home);
|
|
1483
|
+
const tempDir = path.join(uvRoot, 'tmp');
|
|
1484
|
+
const installerName = process.platform === 'win32' ? 'install-uv.ps1' : 'install-uv.sh';
|
|
1485
|
+
const installerUrl =
|
|
1486
|
+
process.platform === 'win32'
|
|
1487
|
+
? 'https://astral.sh/uv/install.ps1'
|
|
1488
|
+
: 'https://astral.sh/uv/install.sh';
|
|
1489
|
+
const installerPath = path.join(tempDir, installerName);
|
|
1490
|
+
|
|
1491
|
+
ensureDir(binDir);
|
|
1492
|
+
ensureDir(tempDir);
|
|
1493
|
+
|
|
1494
|
+
console.log(`DeepScientist is installing a local uv runtime manager under ${binDir}.`);
|
|
1495
|
+
downloadFileWithNode(installerUrl, installerPath);
|
|
1496
|
+
|
|
1497
|
+
const installEnv = {
|
|
1498
|
+
...process.env,
|
|
1499
|
+
UV_UNMANAGED_INSTALL: binDir,
|
|
1500
|
+
};
|
|
1501
|
+
|
|
1502
|
+
let shellBinary;
|
|
1503
|
+
let shellArgs;
|
|
1504
|
+
if (process.platform === 'win32') {
|
|
1505
|
+
shellBinary =
|
|
1506
|
+
resolveExecutableOnPath('powershell.exe')
|
|
1507
|
+
|| resolveExecutableOnPath('powershell')
|
|
1508
|
+
|| resolveExecutableOnPath('pwsh.exe')
|
|
1509
|
+
|| resolveExecutableOnPath('pwsh');
|
|
1510
|
+
if (!shellBinary) {
|
|
1511
|
+
throw new Error('PowerShell is not available to run the official uv installer.');
|
|
1512
|
+
}
|
|
1513
|
+
shellArgs = ['-ExecutionPolicy', 'ByPass', '-File', installerPath];
|
|
1514
|
+
} else {
|
|
1515
|
+
shellBinary = resolveExecutableOnPath('sh');
|
|
1516
|
+
if (!shellBinary) {
|
|
1517
|
+
throw new Error('`sh` is not available to run the official uv installer.');
|
|
1518
|
+
}
|
|
1519
|
+
shellArgs = [installerPath];
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
const installResult = spawnSync(shellBinary, shellArgs, {
|
|
1523
|
+
cwd: repoRoot,
|
|
1524
|
+
stdio: 'inherit',
|
|
1525
|
+
env: installEnv,
|
|
1526
|
+
});
|
|
1527
|
+
if (installResult.error) {
|
|
1528
|
+
throw installResult.error;
|
|
1529
|
+
}
|
|
1530
|
+
if (installResult.status !== 0) {
|
|
1531
|
+
throw new Error(`The official uv installer exited with status ${installResult.status ?? 1}.`);
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
const installedBinary = resolveBinaryReference(runtimeUvBinaryPath(home));
|
|
1535
|
+
if (!installedBinary) {
|
|
1536
|
+
throw new Error(`uv installation finished, but no executable was found under ${binDir}.`);
|
|
1537
|
+
}
|
|
1538
|
+
return installedBinary;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
function ensureUvBinary(home) {
|
|
1542
|
+
const resolved = resolveUvBinary(home);
|
|
1543
|
+
if (resolved.path) {
|
|
1544
|
+
return resolved.path;
|
|
1545
|
+
}
|
|
1546
|
+
if (resolved.source === 'env' && resolved.configured) {
|
|
1547
|
+
throw new Error(`Configured uv binary could not be resolved: ${resolved.configured}`);
|
|
1548
|
+
}
|
|
1549
|
+
return installLocalUv(home);
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
function buildUvRuntimeEnv(home, extraEnv = {}) {
|
|
1553
|
+
return {
|
|
1554
|
+
...process.env,
|
|
1555
|
+
UV_CACHE_DIR: runtimeUvCachePath(home),
|
|
1556
|
+
UV_PROJECT_ENVIRONMENT: runtimePythonEnvPath(home),
|
|
1557
|
+
UV_PYTHON_INSTALL_DIR: runtimeUvPythonInstallPath(home),
|
|
1558
|
+
...extraEnv,
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
function ensureUvLockPresent() {
|
|
1563
|
+
const lockPath = uvLockPath();
|
|
1564
|
+
if (fs.existsSync(lockPath)) {
|
|
1565
|
+
return lockPath;
|
|
1566
|
+
}
|
|
1567
|
+
console.error('DeepScientist is missing `uv.lock` in the installed package.');
|
|
1568
|
+
console.error('Reinstall the npm package, or from a source checkout run `uv lock` and try again.');
|
|
1569
|
+
process.exit(1);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
function resolveUvVersion(uvBinary) {
|
|
1573
|
+
const result = runSync(uvBinary, ['--version'], { capture: true, allowFailure: true });
|
|
1574
|
+
if (result.status !== 0) {
|
|
1575
|
+
return null;
|
|
1576
|
+
}
|
|
1577
|
+
return String(result.stdout || '').trim() || null;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
function ensureUvManagedPython(home, uvBinary, minimumVersionRequest) {
|
|
1581
|
+
ensureDir(runtimeUvPythonInstallPath(home));
|
|
1582
|
+
ensureDir(runtimeUvCachePath(home));
|
|
1583
|
+
step(1, 4, `Provisioning uv-managed Python ${minimumVersionRequest}+`);
|
|
1584
|
+
const installResult = runSync(
|
|
1585
|
+
uvBinary,
|
|
1586
|
+
['python', 'install', minimumVersionRequest],
|
|
1587
|
+
{
|
|
1588
|
+
allowFailure: true,
|
|
1589
|
+
env: buildUvRuntimeEnv(home),
|
|
1590
|
+
}
|
|
1591
|
+
);
|
|
1592
|
+
if (installResult.status !== 0) {
|
|
1593
|
+
console.error('DeepScientist could not install a uv-managed Python runtime.');
|
|
1594
|
+
process.exit(installResult.status ?? 1);
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
const findResult = runSync(
|
|
1598
|
+
uvBinary,
|
|
1599
|
+
['python', 'find', '--managed-python', minimumVersionRequest],
|
|
1600
|
+
{
|
|
1601
|
+
capture: true,
|
|
1602
|
+
allowFailure: true,
|
|
1603
|
+
env: buildUvRuntimeEnv(home),
|
|
1604
|
+
}
|
|
1605
|
+
);
|
|
1606
|
+
const managedPython = String(findResult.stdout || '')
|
|
1607
|
+
.trim()
|
|
1608
|
+
.split(/\r?\n/)
|
|
1609
|
+
.filter(Boolean)
|
|
1610
|
+
.pop();
|
|
1611
|
+
if (!managedPython) {
|
|
1612
|
+
console.error('DeepScientist installed uv-managed Python, but could not locate the interpreter afterward.');
|
|
1613
|
+
process.exit(findResult.status ?? 1);
|
|
1614
|
+
}
|
|
1615
|
+
const probe = decoratePythonProbe(probePython(managedPython), 'uv-managed');
|
|
1616
|
+
if (!probe || !pythonMeetsMinimum(probe)) {
|
|
1617
|
+
console.error('DeepScientist found a uv-managed Python, but it does not satisfy the required version.');
|
|
1618
|
+
process.exit(1);
|
|
1619
|
+
}
|
|
1620
|
+
return probe;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
function syncUvProjectEnvironment(home, uvBinary, pythonTarget, editable) {
|
|
1624
|
+
const args = ['sync', '--frozen', '--no-dev', '--compile-bytecode', '--python', pythonTarget];
|
|
1625
|
+
if (!editable) {
|
|
1626
|
+
args.push('--no-editable');
|
|
1627
|
+
}
|
|
1628
|
+
step(2, 4, 'Syncing locked Python environment');
|
|
1629
|
+
const result = runSync(uvBinary, args, {
|
|
1630
|
+
allowFailure: true,
|
|
1631
|
+
env: buildUvRuntimeEnv(home),
|
|
1632
|
+
});
|
|
1633
|
+
if (result.status === 0) {
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
console.error('DeepScientist could not sync the locked Python environment with uv.');
|
|
1637
|
+
console.error('If you are working from a source checkout, run `uv lock` after dependency changes and try again.');
|
|
1638
|
+
process.exit(result.status ?? 1);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
function createRuntimeSelectionProbe(runtimeProbe, sourceLabel) {
|
|
1642
|
+
return {
|
|
1643
|
+
...runtimeProbe,
|
|
1644
|
+
sourceLabel,
|
|
1645
|
+
};
|
|
472
1646
|
}
|
|
473
1647
|
|
|
474
1648
|
function ensurePythonRuntime(home) {
|
|
475
1649
|
ensureDir(path.join(home, 'runtime'));
|
|
476
1650
|
ensureDir(path.join(home, 'runtime', 'bundle'));
|
|
477
|
-
|
|
1651
|
+
ensureDir(runtimeUvCachePath(home));
|
|
1652
|
+
ensureDir(runtimeUvPythonInstallPath(home));
|
|
1653
|
+
ensureDir(runtimeToolsPath(home));
|
|
1654
|
+
let uvBinary;
|
|
1655
|
+
try {
|
|
1656
|
+
uvBinary = ensureUvBinary(home);
|
|
1657
|
+
} catch (error) {
|
|
1658
|
+
printUvInstallGuidance(home, error instanceof Error ? error.message : String(error));
|
|
1659
|
+
process.exit(1);
|
|
1660
|
+
}
|
|
1661
|
+
const runtimePlan = resolvePythonRuntimePlan();
|
|
1662
|
+
if (runtimePlan.runtimeKind === 'managed') {
|
|
1663
|
+
printManagedPythonFallbackNotice({
|
|
1664
|
+
rejectedProbe: runtimePlan.rejectedProbe || null,
|
|
1665
|
+
source: runtimePlan.source,
|
|
1666
|
+
minimumVersionRequest: runtimePlan.minimumVersionRequest,
|
|
1667
|
+
installDir: runtimeUvPythonInstallPath(home),
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
const lockPath = ensureUvLockPresent();
|
|
478
1671
|
const stampPath = path.join(home, 'runtime', 'bundle', 'python-stamp.json');
|
|
1672
|
+
const editable = useEditableProjectInstall();
|
|
479
1673
|
const desiredStamp = {
|
|
1674
|
+
runtimeManager: 'uv',
|
|
480
1675
|
version: packageJson.version,
|
|
481
1676
|
pyprojectHash: sha256File(path.join(repoRoot, 'pyproject.toml')),
|
|
1677
|
+
uvLockHash: sha256File(lockPath),
|
|
1678
|
+
editable,
|
|
1679
|
+
sourceTreeHash: editable ? null : hashPythonSourceTree(),
|
|
1680
|
+
uvVersion: resolveUvVersion(uvBinary),
|
|
1681
|
+
envPath: runtimePythonEnvPath(home),
|
|
1682
|
+
source:
|
|
1683
|
+
runtimePlan.runtimeKind === 'system'
|
|
1684
|
+
? {
|
|
1685
|
+
kind: 'system',
|
|
1686
|
+
source: runtimePlan.selectedProbe.source,
|
|
1687
|
+
sourceExecutable:
|
|
1688
|
+
runtimePlan.selectedProbe.realExecutable
|
|
1689
|
+
|| runtimePlan.selectedProbe.executable
|
|
1690
|
+
|| runtimePlan.selectedProbe.binary,
|
|
1691
|
+
sourceVersion: runtimePlan.selectedProbe.version,
|
|
1692
|
+
sourceMajorMinor: pythonMajorMinor(runtimePlan.selectedProbe),
|
|
1693
|
+
}
|
|
1694
|
+
: {
|
|
1695
|
+
kind: 'uv-managed',
|
|
1696
|
+
minimumVersionRequest: runtimePlan.minimumVersionRequest,
|
|
1697
|
+
},
|
|
482
1698
|
};
|
|
483
1699
|
|
|
484
1700
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
if (
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
1701
|
+
const runtimePython = runtimePythonPath(home);
|
|
1702
|
+
const currentStamp = readJsonFile(stampPath);
|
|
1703
|
+
const runtimeProbe = fs.existsSync(runtimePython) ? probePython(runtimePython) : null;
|
|
1704
|
+
const runtimeBroken = !runtimeProbe || !runtimeProbe.ok || !pythonMeetsMinimum(runtimeProbe);
|
|
1705
|
+
const stampChanged = JSON.stringify(currentStamp || null) !== JSON.stringify(desiredStamp);
|
|
1706
|
+
|
|
1707
|
+
if (runtimeBroken || stampChanged) {
|
|
1708
|
+
const reason = runtimeBroken
|
|
1709
|
+
? 'DeepScientist is repairing the local uv-managed Python runtime.'
|
|
1710
|
+
: 'DeepScientist detected a runtime change and is rebuilding the local uv-managed environment.';
|
|
1711
|
+
console.warn(reason);
|
|
1712
|
+
fs.rmSync(stampPath, { force: true });
|
|
1713
|
+
fs.rmSync(runtimePythonEnvPath(home), { recursive: true, force: true });
|
|
1714
|
+
|
|
1715
|
+
let pythonTarget = null;
|
|
1716
|
+
let sourceLabel = null;
|
|
1717
|
+
if (runtimePlan.runtimeKind === 'system' && runtimePlan.selectedProbe) {
|
|
1718
|
+
pythonTarget =
|
|
1719
|
+
runtimePlan.selectedProbe.realExecutable
|
|
1720
|
+
|| runtimePlan.selectedProbe.executable
|
|
1721
|
+
|| runtimePlan.selectedProbe.binary;
|
|
1722
|
+
sourceLabel = `${runtimePlan.selectedProbe.sourceLabel} via uv-env`;
|
|
1723
|
+
step(1, 4, 'Preparing uv-managed Python runtime');
|
|
1724
|
+
} else {
|
|
1725
|
+
const managedPython = ensureUvManagedPython(home, uvBinary, runtimePlan.minimumVersionRequest);
|
|
1726
|
+
pythonTarget = managedPython.realExecutable || managedPython.executable || managedPython.binary;
|
|
1727
|
+
sourceLabel = managedPython.sourceLabel;
|
|
496
1728
|
}
|
|
497
|
-
}
|
|
498
1729
|
|
|
499
|
-
|
|
500
|
-
step(2, 4, 'Installing Python package and dependencies');
|
|
501
|
-
installPythonBundle(venvPython);
|
|
1730
|
+
syncUvProjectEnvironment(home, uvBinary, pythonTarget, editable);
|
|
502
1731
|
fs.writeFileSync(stampPath, `${JSON.stringify(desiredStamp, null, 2)}\n`, 'utf8');
|
|
1732
|
+
const syncedProbe = fs.existsSync(runtimePython) ? probePython(runtimePython) : null;
|
|
1733
|
+
if (syncedProbe && syncedProbe.ok && pythonMeetsMinimum(syncedProbe) && verifyPythonRuntime(runtimePython)) {
|
|
1734
|
+
fs.rmSync(legacyVenvRootPath(home), { recursive: true, force: true });
|
|
1735
|
+
return {
|
|
1736
|
+
runtimePython,
|
|
1737
|
+
uvBinary,
|
|
1738
|
+
runtimeManager: 'uv',
|
|
1739
|
+
runtimeProbe: createRuntimeSelectionProbe(syncedProbe, sourceLabel || 'uv-managed'),
|
|
1740
|
+
sourcePython: runtimePlan.selectedProbe || null,
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
503
1743
|
}
|
|
504
1744
|
|
|
505
|
-
if (verifyPythonRuntime(
|
|
506
|
-
|
|
1745
|
+
if (runtimeProbe && runtimeProbe.ok && pythonMeetsMinimum(runtimeProbe) && verifyPythonRuntime(runtimePython)) {
|
|
1746
|
+
fs.rmSync(legacyVenvRootPath(home), { recursive: true, force: true });
|
|
1747
|
+
return {
|
|
1748
|
+
runtimePython,
|
|
1749
|
+
uvBinary,
|
|
1750
|
+
runtimeManager: 'uv',
|
|
1751
|
+
runtimeProbe: createRuntimeSelectionProbe(
|
|
1752
|
+
runtimeProbe,
|
|
1753
|
+
runtimePlan.runtimeKind === 'system' && runtimePlan.selectedProbe
|
|
1754
|
+
? `${runtimePlan.selectedProbe.sourceLabel} via uv-env`
|
|
1755
|
+
: 'uv-managed'
|
|
1756
|
+
),
|
|
1757
|
+
sourcePython: runtimePlan.selectedProbe || null,
|
|
1758
|
+
};
|
|
507
1759
|
}
|
|
508
1760
|
|
|
509
|
-
console.warn('DeepScientist is
|
|
1761
|
+
console.warn('DeepScientist is retrying the local uv-managed Python runtime repair.');
|
|
510
1762
|
fs.rmSync(stampPath, { force: true });
|
|
511
|
-
fs.rmSync(
|
|
1763
|
+
fs.rmSync(runtimePythonEnvPath(home), { recursive: true, force: true });
|
|
512
1764
|
}
|
|
513
1765
|
|
|
514
|
-
console.error('DeepScientist could not prepare a healthy
|
|
1766
|
+
console.error('DeepScientist could not prepare a healthy uv-managed Python runtime.');
|
|
515
1767
|
process.exit(1);
|
|
516
1768
|
}
|
|
517
1769
|
|
|
518
|
-
function runPythonCli(
|
|
519
|
-
|
|
1770
|
+
function runPythonCli(runtimePython, args, options = {}) {
|
|
1771
|
+
const env = {
|
|
1772
|
+
...process.env,
|
|
1773
|
+
DEEPSCIENTIST_REPO_ROOT: repoRoot,
|
|
1774
|
+
...(options.env || {}),
|
|
1775
|
+
};
|
|
1776
|
+
return runSync(runtimePython, ['-m', 'deepscientist.cli', ...args], { ...options, env });
|
|
520
1777
|
}
|
|
521
1778
|
|
|
522
1779
|
function normalizePythonCliArgs(args, home) {
|
|
@@ -532,7 +1789,7 @@ function normalizePythonCliArgs(args, home) {
|
|
|
532
1789
|
return ['--home', home, ...normalized];
|
|
533
1790
|
}
|
|
534
1791
|
|
|
535
|
-
function ensureInitialized(home,
|
|
1792
|
+
function ensureInitialized(home, runtimePython) {
|
|
536
1793
|
const stampPath = path.join(home, 'runtime', 'bundle', 'init-stamp.json');
|
|
537
1794
|
let currentStamp = null;
|
|
538
1795
|
if (fs.existsSync(stampPath)) {
|
|
@@ -557,7 +1814,7 @@ function ensureInitialized(home, venvPython) {
|
|
|
557
1814
|
return;
|
|
558
1815
|
}
|
|
559
1816
|
step(3, 4, 'Preparing DeepScientist home, config, skills, and Git checks');
|
|
560
|
-
const result = runPythonCli(
|
|
1817
|
+
const result = runPythonCli(runtimePython, ['--home', home, 'init'], { capture: true, allowFailure: true });
|
|
561
1818
|
const stdout = result.stdout || '';
|
|
562
1819
|
let payload = {};
|
|
563
1820
|
try {
|
|
@@ -616,34 +1873,11 @@ function resolveExecutableOnPath(commandName) {
|
|
|
616
1873
|
return null;
|
|
617
1874
|
}
|
|
618
1875
|
const directories = pathValue.split(path.delimiter).filter(Boolean);
|
|
619
|
-
const extensions =
|
|
620
|
-
process.platform === 'win32'
|
|
621
|
-
? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM')
|
|
622
|
-
.split(';')
|
|
623
|
-
.filter(Boolean)
|
|
624
|
-
: [''];
|
|
625
1876
|
for (const directory of directories) {
|
|
626
1877
|
const base = path.join(directory, commandName);
|
|
627
|
-
for (const
|
|
628
|
-
|
|
629
|
-
try {
|
|
630
|
-
if (!fs.existsSync(candidate)) {
|
|
631
|
-
continue;
|
|
632
|
-
}
|
|
633
|
-
const stat = fs.statSync(candidate);
|
|
634
|
-
if (!stat.isFile()) {
|
|
635
|
-
continue;
|
|
636
|
-
}
|
|
637
|
-
if (process.platform !== 'win32') {
|
|
638
|
-
try {
|
|
639
|
-
fs.accessSync(candidate, fs.constants.X_OK);
|
|
640
|
-
} catch {
|
|
641
|
-
continue;
|
|
642
|
-
}
|
|
643
|
-
}
|
|
1878
|
+
for (const candidate of candidateExecutablePaths(base)) {
|
|
1879
|
+
if (isExecutableFile(candidate)) {
|
|
644
1880
|
return candidate;
|
|
645
|
-
} catch {
|
|
646
|
-
continue;
|
|
647
1881
|
}
|
|
648
1882
|
}
|
|
649
1883
|
}
|
|
@@ -956,40 +2190,523 @@ async function stopDaemon(home) {
|
|
|
956
2190
|
console.log('DeepScientist daemon stopped.');
|
|
957
2191
|
}
|
|
958
2192
|
|
|
959
|
-
|
|
2193
|
+
function writeUpdateLog(home, content) {
|
|
2194
|
+
const logPath = path.join(home, 'logs', 'update.log');
|
|
2195
|
+
ensureDir(path.dirname(logPath));
|
|
2196
|
+
fs.appendFileSync(logPath, `${content.replace(/\s+$/, '')}\n`, 'utf8');
|
|
2197
|
+
return logPath;
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
function summarizeUpdateFailure(result) {
|
|
2201
|
+
const lines = [];
|
|
2202
|
+
if (result.error) {
|
|
2203
|
+
lines.push(result.error);
|
|
2204
|
+
}
|
|
2205
|
+
if (result.stderr) {
|
|
2206
|
+
lines.push(String(result.stderr).trim());
|
|
2207
|
+
}
|
|
2208
|
+
if (result.stdout) {
|
|
2209
|
+
lines.push(String(result.stdout).trim());
|
|
2210
|
+
}
|
|
2211
|
+
return lines.filter(Boolean).join('\n').trim() || 'Unknown update failure.';
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
function runNpmInstallLatest(home, npmBinary) {
|
|
2215
|
+
const args = ['install', '-g', `${UPDATE_PACKAGE_NAME}@latest`, '--no-audit', '--no-fund'];
|
|
2216
|
+
const startedAt = new Date().toISOString();
|
|
2217
|
+
const result = spawnSync(npmBinary, args, {
|
|
2218
|
+
encoding: 'utf8',
|
|
2219
|
+
env: process.env,
|
|
2220
|
+
timeout: 15 * 60 * 1000,
|
|
2221
|
+
});
|
|
2222
|
+
const finishedAt = new Date().toISOString();
|
|
2223
|
+
const logPath = writeUpdateLog(
|
|
2224
|
+
home,
|
|
2225
|
+
[
|
|
2226
|
+
`=== ${startedAt} installing ${UPDATE_PACKAGE_NAME}@latest ===`,
|
|
2227
|
+
`$ ${npmBinary} ${args.join(' ')}`,
|
|
2228
|
+
String(result.stdout || '').trim(),
|
|
2229
|
+
String(result.stderr || '').trim(),
|
|
2230
|
+
`exit=${result.status ?? 'null'} error=${result.error ? result.error.message : 'none'}`,
|
|
2231
|
+
`=== finished ${finishedAt} ===`,
|
|
2232
|
+
'',
|
|
2233
|
+
].join('\n')
|
|
2234
|
+
);
|
|
2235
|
+
return {
|
|
2236
|
+
ok: !result.error && result.status === 0,
|
|
2237
|
+
stdout: String(result.stdout || ''),
|
|
2238
|
+
stderr: String(result.stderr || ''),
|
|
2239
|
+
error: result.error ? result.error.message : null,
|
|
2240
|
+
status: result.status ?? null,
|
|
2241
|
+
logPath,
|
|
2242
|
+
};
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
async function promptUpdateAction(status) {
|
|
2246
|
+
const options = [
|
|
2247
|
+
{
|
|
2248
|
+
value: 'update',
|
|
2249
|
+
label: status.can_self_update ? 'Update now' : 'Show manual update',
|
|
2250
|
+
},
|
|
2251
|
+
{ value: 'later', label: 'Remind me later' },
|
|
2252
|
+
{ value: 'skip', label: 'Skip this version' },
|
|
2253
|
+
];
|
|
2254
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2255
|
+
return 'later';
|
|
2256
|
+
}
|
|
2257
|
+
return new Promise((resolve) => {
|
|
2258
|
+
let selected = 1;
|
|
2259
|
+
const lines = [
|
|
2260
|
+
'',
|
|
2261
|
+
'A new DeepScientist version is available.',
|
|
2262
|
+
'',
|
|
2263
|
+
`Current: ${status.current_version}`,
|
|
2264
|
+
`Latest: ${status.latest_version || 'unknown'}`,
|
|
2265
|
+
'',
|
|
2266
|
+
status.can_self_update
|
|
2267
|
+
? 'What do you want to do?'
|
|
2268
|
+
: 'Self-update is not available for this installation. Choose an action:',
|
|
2269
|
+
];
|
|
2270
|
+
|
|
2271
|
+
const cleanup = () => {
|
|
2272
|
+
process.stdin.off('keypress', onKeypress);
|
|
2273
|
+
if (process.stdin.isTTY) {
|
|
2274
|
+
process.stdin.setRawMode(false);
|
|
2275
|
+
}
|
|
2276
|
+
process.stdin.pause();
|
|
2277
|
+
console.log('');
|
|
2278
|
+
};
|
|
2279
|
+
|
|
2280
|
+
const render = () => {
|
|
2281
|
+
console.clear();
|
|
2282
|
+
for (const line of lines) {
|
|
2283
|
+
console.log(line);
|
|
2284
|
+
}
|
|
2285
|
+
for (let index = 0; index < options.length; index += 1) {
|
|
2286
|
+
const option = options[index];
|
|
2287
|
+
console.log(`${index === selected ? '>' : ' '} ${option.label}`);
|
|
2288
|
+
}
|
|
2289
|
+
console.log('');
|
|
2290
|
+
console.log('Use ↑/↓ and Enter.');
|
|
2291
|
+
};
|
|
2292
|
+
|
|
2293
|
+
const onKeypress = (_str, key) => {
|
|
2294
|
+
if (key?.name === 'up') {
|
|
2295
|
+
selected = (selected - 1 + options.length) % options.length;
|
|
2296
|
+
render();
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2299
|
+
if (key?.name === 'down') {
|
|
2300
|
+
selected = (selected + 1) % options.length;
|
|
2301
|
+
render();
|
|
2302
|
+
return;
|
|
2303
|
+
}
|
|
2304
|
+
if (key?.name === 'return') {
|
|
2305
|
+
const choice = options[selected].value;
|
|
2306
|
+
cleanup();
|
|
2307
|
+
resolve(choice);
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
2310
|
+
if (key?.ctrl && key?.name === 'c') {
|
|
2311
|
+
cleanup();
|
|
2312
|
+
resolve('later');
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2315
|
+
|
|
2316
|
+
readline.emitKeypressEvents(process.stdin);
|
|
2317
|
+
process.stdin.setRawMode(true);
|
|
2318
|
+
process.stdin.resume();
|
|
2319
|
+
process.stdin.on('keypress', onKeypress);
|
|
2320
|
+
render();
|
|
2321
|
+
});
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
function printUpdateStatus(status, { compact = false } = {}) {
|
|
2325
|
+
if (compact) {
|
|
2326
|
+
if (status.update_available) {
|
|
2327
|
+
console.log(
|
|
2328
|
+
`DeepScientist update available: ${status.current_version} -> ${status.latest_version}`
|
|
2329
|
+
);
|
|
2330
|
+
if (status.can_self_update) {
|
|
2331
|
+
console.log('Run `ds update --yes` to install it.');
|
|
2332
|
+
} else {
|
|
2333
|
+
console.log(`Manual update: ${status.manual_update_command}`);
|
|
2334
|
+
}
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
console.log(`DeepScientist is up to date (${status.current_version}).`);
|
|
2338
|
+
return;
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
console.log('DeepScientist update status');
|
|
2342
|
+
renderKeyValueRows([
|
|
2343
|
+
['Current', status.current_version],
|
|
2344
|
+
['Latest', status.latest_version || 'unknown'],
|
|
2345
|
+
['Available', status.update_available ? 'yes' : 'no'],
|
|
2346
|
+
['Install mode', status.install_mode],
|
|
2347
|
+
['Self-update', status.can_self_update ? 'supported' : 'manual-only'],
|
|
2348
|
+
['Last checked', status.last_checked_at || 'never'],
|
|
2349
|
+
]);
|
|
2350
|
+
if (status.last_check_error) {
|
|
2351
|
+
console.log('');
|
|
2352
|
+
console.log(`Version check error: ${status.last_check_error}`);
|
|
2353
|
+
}
|
|
2354
|
+
if (!status.can_self_update) {
|
|
2355
|
+
console.log('');
|
|
2356
|
+
console.log(`Manual update command: ${status.manual_update_command}`);
|
|
2357
|
+
if (status.reason) {
|
|
2358
|
+
console.log(status.reason);
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
const npmBinary = resolveNpmBinary();
|
|
2362
|
+
if (!npmBinary) {
|
|
2363
|
+
return {
|
|
2364
|
+
ok: false,
|
|
2365
|
+
updated: false,
|
|
2366
|
+
status,
|
|
2367
|
+
message: '`npm` is not available on PATH.',
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
function spawnDetachedNode(args, options = {}) {
|
|
2373
|
+
const out = options.logPath ? fs.openSync(options.logPath, 'a') : 'ignore';
|
|
2374
|
+
const child = spawn(process.execPath, args, {
|
|
2375
|
+
cwd: options.cwd || repoRoot,
|
|
2376
|
+
detached: true,
|
|
2377
|
+
stdio: ['ignore', out, out],
|
|
2378
|
+
env: options.env || process.env,
|
|
2379
|
+
});
|
|
2380
|
+
child.unref();
|
|
2381
|
+
return child;
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
async function restartIntoUpdatedLauncher(rawArgs) {
|
|
2385
|
+
const launcherPath = resolveLauncherPath();
|
|
2386
|
+
if (!launcherPath) {
|
|
2387
|
+
throw new Error('Could not resolve the DeepScientist launcher after the update.');
|
|
2388
|
+
}
|
|
2389
|
+
const args = [launcherPath, '--skip-update-check', ...rawArgs.filter((item) => item !== '--skip-update-check')];
|
|
2390
|
+
const child = spawn(process.execPath, args, {
|
|
2391
|
+
cwd: repoRoot,
|
|
2392
|
+
stdio: 'inherit',
|
|
2393
|
+
env: process.env,
|
|
2394
|
+
});
|
|
2395
|
+
await new Promise((resolve, reject) => {
|
|
2396
|
+
child.on('error', reject);
|
|
2397
|
+
child.on('exit', (code) => {
|
|
2398
|
+
process.exit(code ?? 0);
|
|
2399
|
+
resolve();
|
|
2400
|
+
});
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
async function performSelfUpdate(home, options = {}) {
|
|
2405
|
+
const status = checkForUpdates(home, { force: true });
|
|
2406
|
+
if (!status.update_available) {
|
|
2407
|
+
return {
|
|
2408
|
+
ok: true,
|
|
2409
|
+
updated: false,
|
|
2410
|
+
status,
|
|
2411
|
+
message: `DeepScientist is already on the latest version (${status.current_version}).`,
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
2414
|
+
if (!status.can_self_update) {
|
|
2415
|
+
return {
|
|
2416
|
+
ok: false,
|
|
2417
|
+
updated: false,
|
|
2418
|
+
status,
|
|
2419
|
+
message: status.reason || `Manual update required: ${status.manual_update_command}`,
|
|
2420
|
+
};
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
const daemonState = readDaemonState(home);
|
|
2424
|
+
const configuredUi = readConfiguredUiAddressFromFile(home, options.host, options.port);
|
|
2425
|
+
const host = options.host || daemonState?.host || configuredUi.host;
|
|
2426
|
+
const port = options.port || daemonState?.port || configuredUi.port;
|
|
2427
|
+
const targetVersion = status.latest_version;
|
|
2428
|
+
|
|
2429
|
+
mergeUpdateState(home, {
|
|
2430
|
+
current_version: status.current_version,
|
|
2431
|
+
latest_version: targetVersion,
|
|
2432
|
+
target_version: targetVersion,
|
|
2433
|
+
busy: true,
|
|
2434
|
+
last_update_started_at: new Date().toISOString(),
|
|
2435
|
+
last_update_result: null,
|
|
2436
|
+
});
|
|
2437
|
+
|
|
2438
|
+
try {
|
|
2439
|
+
if (daemonState?.pid || daemonState?.daemon_id) {
|
|
2440
|
+
await stopDaemon(home);
|
|
2441
|
+
}
|
|
2442
|
+
} catch (error) {
|
|
2443
|
+
mergeUpdateState(home, {
|
|
2444
|
+
busy: false,
|
|
2445
|
+
last_update_finished_at: new Date().toISOString(),
|
|
2446
|
+
last_update_result: {
|
|
2447
|
+
ok: false,
|
|
2448
|
+
target_version: targetVersion,
|
|
2449
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2450
|
+
},
|
|
2451
|
+
});
|
|
2452
|
+
return {
|
|
2453
|
+
ok: false,
|
|
2454
|
+
updated: false,
|
|
2455
|
+
status,
|
|
2456
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
const installResult = runNpmInstallLatest(home, npmBinary);
|
|
2461
|
+
if (!installResult.ok) {
|
|
2462
|
+
const message = summarizeUpdateFailure(installResult);
|
|
2463
|
+
mergeUpdateState(home, {
|
|
2464
|
+
busy: false,
|
|
2465
|
+
last_update_finished_at: new Date().toISOString(),
|
|
2466
|
+
last_update_result: {
|
|
2467
|
+
ok: false,
|
|
2468
|
+
target_version: targetVersion,
|
|
2469
|
+
message,
|
|
2470
|
+
log_path: installResult.logPath,
|
|
2471
|
+
},
|
|
2472
|
+
});
|
|
2473
|
+
return {
|
|
2474
|
+
ok: false,
|
|
2475
|
+
updated: false,
|
|
2476
|
+
status,
|
|
2477
|
+
message,
|
|
2478
|
+
log_path: installResult.logPath,
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
const restartDaemon =
|
|
2483
|
+
options.restartDaemon === true
|
|
2484
|
+
|| (options.restartDaemon !== false && Boolean(daemonState?.pid || daemonState?.daemon_id));
|
|
2485
|
+
if (restartDaemon) {
|
|
2486
|
+
const launcherPath = resolveLauncherPath();
|
|
2487
|
+
if (!launcherPath) {
|
|
2488
|
+
const message = 'DeepScientist was updated, but the new launcher path could not be resolved for daemon restart.';
|
|
2489
|
+
mergeUpdateState(home, {
|
|
2490
|
+
busy: false,
|
|
2491
|
+
last_update_finished_at: new Date().toISOString(),
|
|
2492
|
+
last_update_result: {
|
|
2493
|
+
ok: false,
|
|
2494
|
+
target_version: targetVersion,
|
|
2495
|
+
message,
|
|
2496
|
+
log_path: installResult.logPath,
|
|
2497
|
+
},
|
|
2498
|
+
});
|
|
2499
|
+
return {
|
|
2500
|
+
ok: false,
|
|
2501
|
+
updated: true,
|
|
2502
|
+
status,
|
|
2503
|
+
message,
|
|
2504
|
+
log_path: installResult.logPath,
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
spawnDetachedNode(
|
|
2508
|
+
[
|
|
2509
|
+
launcherPath,
|
|
2510
|
+
'--home',
|
|
2511
|
+
home,
|
|
2512
|
+
'--host',
|
|
2513
|
+
String(host),
|
|
2514
|
+
'--port',
|
|
2515
|
+
String(port),
|
|
2516
|
+
'--daemon-only',
|
|
2517
|
+
'--no-browser',
|
|
2518
|
+
'--skip-update-check',
|
|
2519
|
+
],
|
|
2520
|
+
{
|
|
2521
|
+
cwd: repoRoot,
|
|
2522
|
+
env: process.env,
|
|
2523
|
+
logPath: path.join(home, 'logs', 'daemon-restart.log'),
|
|
2524
|
+
}
|
|
2525
|
+
);
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
mergeUpdateState(home, {
|
|
2529
|
+
busy: false,
|
|
2530
|
+
current_version: targetVersion,
|
|
2531
|
+
latest_version: targetVersion,
|
|
2532
|
+
target_version: null,
|
|
2533
|
+
last_checked_at: new Date().toISOString(),
|
|
2534
|
+
last_check_error: null,
|
|
2535
|
+
last_update_finished_at: new Date().toISOString(),
|
|
2536
|
+
last_update_result: {
|
|
2537
|
+
ok: true,
|
|
2538
|
+
target_version: targetVersion,
|
|
2539
|
+
message: restartDaemon
|
|
2540
|
+
? `DeepScientist updated to ${targetVersion}. The daemon is restarting.`
|
|
2541
|
+
: `DeepScientist updated to ${targetVersion}.`,
|
|
2542
|
+
log_path: installResult.logPath,
|
|
2543
|
+
},
|
|
2544
|
+
});
|
|
2545
|
+
|
|
2546
|
+
return {
|
|
2547
|
+
ok: true,
|
|
2548
|
+
updated: true,
|
|
2549
|
+
status: buildUpdateStatus(home),
|
|
2550
|
+
message: restartDaemon
|
|
2551
|
+
? `DeepScientist updated to ${targetVersion}. The daemon is restarting.`
|
|
2552
|
+
: `DeepScientist updated to ${targetVersion}.`,
|
|
2553
|
+
log_path: installResult.logPath,
|
|
2554
|
+
};
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
async function maybeHandleStartupUpdate(home, rawArgs, options = {}) {
|
|
2558
|
+
if (options.skipUpdateCheck || process.env.DS_SKIP_UPDATE_PROMPT === '1') {
|
|
2559
|
+
return false;
|
|
2560
|
+
}
|
|
2561
|
+
const status = checkForUpdates(home, { force: false });
|
|
2562
|
+
if (!status.update_available) {
|
|
2563
|
+
return false;
|
|
2564
|
+
}
|
|
2565
|
+
if (!status.prompt_recommended) {
|
|
2566
|
+
return false;
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2570
|
+
printUpdateStatus(status, { compact: true });
|
|
2571
|
+
mergeUpdateState(home, {
|
|
2572
|
+
last_prompted_at: new Date().toISOString(),
|
|
2573
|
+
});
|
|
2574
|
+
return false;
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
const action = await promptUpdateAction(status);
|
|
2578
|
+
if (action === 'later') {
|
|
2579
|
+
markUpdateDeferred(home, status.latest_version);
|
|
2580
|
+
return false;
|
|
2581
|
+
}
|
|
2582
|
+
if (action === 'skip') {
|
|
2583
|
+
markUpdateSkipped(home, status.latest_version);
|
|
2584
|
+
return false;
|
|
2585
|
+
}
|
|
2586
|
+
if (action === 'update' && !status.can_self_update) {
|
|
2587
|
+
console.log(`Manual update required: ${status.manual_update_command}`);
|
|
2588
|
+
if (status.reason) {
|
|
2589
|
+
console.log(status.reason);
|
|
2590
|
+
}
|
|
2591
|
+
markUpdateDeferred(home, status.latest_version);
|
|
2592
|
+
return false;
|
|
2593
|
+
}
|
|
2594
|
+
if (action === 'update') {
|
|
2595
|
+
console.log(`Updating DeepScientist ${status.current_version} -> ${status.latest_version} ...`);
|
|
2596
|
+
const result = await performSelfUpdate(home, { restartDaemon: false });
|
|
2597
|
+
if (!result.ok) {
|
|
2598
|
+
console.error(result.message);
|
|
2599
|
+
return false;
|
|
2600
|
+
}
|
|
2601
|
+
console.log(result.message);
|
|
2602
|
+
await restartIntoUpdatedLauncher(rawArgs);
|
|
2603
|
+
return true;
|
|
2604
|
+
}
|
|
2605
|
+
return false;
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
async function startBackgroundUpdateWorker(home, options = {}) {
|
|
2609
|
+
const launcherPath = resolveLauncherPath();
|
|
2610
|
+
if (!launcherPath) {
|
|
2611
|
+
return {
|
|
2612
|
+
ok: false,
|
|
2613
|
+
started: false,
|
|
2614
|
+
message: 'Could not resolve the launcher path for the background update worker.',
|
|
2615
|
+
};
|
|
2616
|
+
}
|
|
2617
|
+
const status = checkForUpdates(home, { force: false });
|
|
2618
|
+
mergeUpdateState(home, {
|
|
2619
|
+
current_version: status.current_version,
|
|
2620
|
+
latest_version: status.latest_version,
|
|
2621
|
+
target_version: status.latest_version,
|
|
2622
|
+
busy: true,
|
|
2623
|
+
last_update_started_at: new Date().toISOString(),
|
|
2624
|
+
last_update_result: null,
|
|
2625
|
+
});
|
|
2626
|
+
const workerArgs = [
|
|
2627
|
+
launcherPath,
|
|
2628
|
+
'update',
|
|
2629
|
+
'--yes',
|
|
2630
|
+
'--worker',
|
|
2631
|
+
'--home',
|
|
2632
|
+
home,
|
|
2633
|
+
'--host',
|
|
2634
|
+
String(options.host || '0.0.0.0'),
|
|
2635
|
+
'--port',
|
|
2636
|
+
String(options.port || 20999),
|
|
2637
|
+
'--restart-daemon',
|
|
2638
|
+
'--skip-update-check',
|
|
2639
|
+
];
|
|
2640
|
+
spawnDetachedNode(workerArgs, {
|
|
2641
|
+
cwd: repoRoot,
|
|
2642
|
+
env: process.env,
|
|
2643
|
+
logPath: path.join(home, 'logs', 'update-worker.log'),
|
|
2644
|
+
});
|
|
2645
|
+
return {
|
|
2646
|
+
ok: true,
|
|
2647
|
+
started: true,
|
|
2648
|
+
message: 'DeepScientist update worker started.',
|
|
2649
|
+
status: buildUpdateStatus(home),
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
async function readConfiguredUiAddress(home, runtimePython, fallbackHost, fallbackPort) {
|
|
960
2654
|
try {
|
|
961
|
-
const result = runPythonCli(
|
|
2655
|
+
const result = runPythonCli(runtimePython, ['--home', home, 'config', 'show', 'config'], { capture: true, allowFailure: true });
|
|
962
2656
|
const text = result.stdout || '';
|
|
963
2657
|
const hostMatch = text.match(/^\s*host:\s*["']?([^"'\n]+)["']?\s*$/m);
|
|
964
2658
|
const portMatch = text.match(/^\s*port:\s*(\d+)\s*$/m);
|
|
2659
|
+
const modeMatch = text.match(/^\s*default_mode:\s*["']?([^"'\n]+)["']?\s*$/m);
|
|
2660
|
+
const autoOpenMatch = text.match(/^\s*auto_open_browser:\s*([^\n]+)\s*$/m);
|
|
965
2661
|
return {
|
|
966
2662
|
host: fallbackHost || (hostMatch ? hostMatch[1].trim() : '0.0.0.0'),
|
|
967
2663
|
port: fallbackPort || (portMatch ? Number(portMatch[1]) : 20999),
|
|
2664
|
+
defaultMode: normalizeMode(modeMatch ? modeMatch[1].trim() : 'web'),
|
|
2665
|
+
autoOpenBrowser: parseBooleanSetting(autoOpenMatch ? autoOpenMatch[1].trim() : true, true),
|
|
968
2666
|
};
|
|
969
2667
|
} catch {
|
|
970
|
-
return {
|
|
2668
|
+
return {
|
|
2669
|
+
host: fallbackHost || '0.0.0.0',
|
|
2670
|
+
port: fallbackPort || 20999,
|
|
2671
|
+
defaultMode: 'web',
|
|
2672
|
+
autoOpenBrowser: true,
|
|
2673
|
+
};
|
|
971
2674
|
}
|
|
972
2675
|
}
|
|
973
2676
|
|
|
974
2677
|
function readConfiguredUiAddressFromFile(home, fallbackHost, fallbackPort) {
|
|
975
2678
|
const configPath = path.join(home, 'config', 'config.yaml');
|
|
976
2679
|
if (!fs.existsSync(configPath)) {
|
|
977
|
-
return {
|
|
2680
|
+
return {
|
|
2681
|
+
host: fallbackHost || '0.0.0.0',
|
|
2682
|
+
port: fallbackPort || 20999,
|
|
2683
|
+
defaultMode: 'web',
|
|
2684
|
+
autoOpenBrowser: true,
|
|
2685
|
+
};
|
|
978
2686
|
}
|
|
979
2687
|
try {
|
|
980
2688
|
const text = fs.readFileSync(configPath, 'utf8');
|
|
981
2689
|
const hostMatch = text.match(/^\s*host:\s*["']?([^"'\n]+)["']?\s*$/m);
|
|
982
2690
|
const portMatch = text.match(/^\s*port:\s*(\d+)\s*$/m);
|
|
2691
|
+
const modeMatch = text.match(/^\s*default_mode:\s*["']?([^"'\n]+)["']?\s*$/m);
|
|
2692
|
+
const autoOpenMatch = text.match(/^\s*auto_open_browser:\s*([^\n]+)\s*$/m);
|
|
983
2693
|
return {
|
|
984
2694
|
host: fallbackHost || (hostMatch ? hostMatch[1].trim() : '0.0.0.0'),
|
|
985
2695
|
port: fallbackPort || (portMatch ? Number(portMatch[1]) : 20999),
|
|
2696
|
+
defaultMode: normalizeMode(modeMatch ? modeMatch[1].trim() : 'web'),
|
|
2697
|
+
autoOpenBrowser: parseBooleanSetting(autoOpenMatch ? autoOpenMatch[1].trim() : true, true),
|
|
986
2698
|
};
|
|
987
2699
|
} catch {
|
|
988
|
-
return {
|
|
2700
|
+
return {
|
|
2701
|
+
host: fallbackHost || '0.0.0.0',
|
|
2702
|
+
port: fallbackPort || 20999,
|
|
2703
|
+
defaultMode: 'web',
|
|
2704
|
+
autoOpenBrowser: true,
|
|
2705
|
+
};
|
|
989
2706
|
}
|
|
990
2707
|
}
|
|
991
2708
|
|
|
992
|
-
async function startDaemon(home,
|
|
2709
|
+
async function startDaemon(home, runtimePython, host, port) {
|
|
993
2710
|
const browserUrl = browserUiUrl(host, port);
|
|
994
2711
|
const daemonBindUrl = bindUiUrl(host, port);
|
|
995
2712
|
const state = readDaemonState(home);
|
|
@@ -1013,10 +2730,10 @@ async function startDaemon(home, venvPython, host, port) {
|
|
|
1013
2730
|
removeDaemonState(home);
|
|
1014
2731
|
}
|
|
1015
2732
|
|
|
1016
|
-
const bootstrapState = readCodexBootstrapState(home,
|
|
2733
|
+
const bootstrapState = readCodexBootstrapState(home, runtimePython);
|
|
1017
2734
|
if (!bootstrapState.codex_ready) {
|
|
1018
2735
|
console.log('Codex is not marked ready yet. Running startup probe...');
|
|
1019
|
-
const probe = probeCodexBootstrap(home,
|
|
2736
|
+
const probe = probeCodexBootstrap(home, runtimePython);
|
|
1020
2737
|
if (!probe || probe.ok !== true) {
|
|
1021
2738
|
throw createCodexPreflightError(home, probe);
|
|
1022
2739
|
}
|
|
@@ -1029,7 +2746,7 @@ async function startDaemon(home, venvPython, host, port) {
|
|
|
1029
2746
|
const out = fs.openSync(logPath, 'a');
|
|
1030
2747
|
const daemonId = crypto.randomUUID();
|
|
1031
2748
|
const child = spawn(
|
|
1032
|
-
|
|
2749
|
+
runtimePython,
|
|
1033
2750
|
['-m', 'deepscientist.cli', '--home', home, 'daemon', '--host', host, '--port', String(port)],
|
|
1034
2751
|
{
|
|
1035
2752
|
cwd: repoRoot,
|
|
@@ -1037,11 +2754,11 @@ async function startDaemon(home, venvPython, host, port) {
|
|
|
1037
2754
|
stdio: ['ignore', out, out],
|
|
1038
2755
|
env: {
|
|
1039
2756
|
...process.env,
|
|
2757
|
+
DEEPSCIENTIST_REPO_ROOT: repoRoot,
|
|
2758
|
+
DEEPSCIENTIST_NODE_BINARY: process.execPath,
|
|
2759
|
+
DEEPSCIENTIST_LAUNCHER_PATH: path.join(repoRoot, 'bin', 'ds.js'),
|
|
1040
2760
|
DS_DAEMON_ID: daemonId,
|
|
1041
2761
|
DS_DAEMON_MANAGED_BY: 'ds-launcher',
|
|
1042
|
-
PYTHONPATH: process.env.PYTHONPATH
|
|
1043
|
-
? `${srcPath}${path.delimiter}${process.env.PYTHONPATH}`
|
|
1044
|
-
: srcPath,
|
|
1045
2762
|
},
|
|
1046
2763
|
}
|
|
1047
2764
|
);
|
|
@@ -1080,38 +2797,42 @@ async function startDaemon(home, venvPython, host, port) {
|
|
|
1080
2797
|
}
|
|
1081
2798
|
|
|
1082
2799
|
function openBrowser(url) {
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
const child = spawn(
|
|
1086
|
-
child.on('error', () => {
|
|
1087
|
-
console.log(`Open this URL in your browser: ${url}`);
|
|
1088
|
-
});
|
|
2800
|
+
const spawnDetached = (command, args) => {
|
|
2801
|
+
try {
|
|
2802
|
+
const child = spawn(command, args, { detached: true, stdio: 'ignore' });
|
|
1089
2803
|
child.unref();
|
|
1090
2804
|
return true;
|
|
2805
|
+
} catch {
|
|
2806
|
+
return false;
|
|
1091
2807
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
2808
|
+
};
|
|
2809
|
+
|
|
2810
|
+
if (process.platform === 'darwin') {
|
|
2811
|
+
const opener = resolveExecutableOnPath('open');
|
|
2812
|
+
return opener ? spawnDetached(opener, [url]) : false;
|
|
2813
|
+
}
|
|
2814
|
+
if (process.platform === 'win32') {
|
|
2815
|
+
return spawnDetached('cmd', ['/c', 'start', '', url]);
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
const commands = [
|
|
2819
|
+
{ command: 'xdg-open', args: [url] },
|
|
2820
|
+
{ command: 'gio', args: ['open', url] },
|
|
2821
|
+
{ command: 'sensible-browser', args: [url] },
|
|
2822
|
+
{ command: 'gnome-open', args: [url] },
|
|
2823
|
+
{ command: 'kde-open', args: [url] },
|
|
2824
|
+
{ command: 'kde-open5', args: [url] },
|
|
2825
|
+
];
|
|
2826
|
+
for (const candidate of commands) {
|
|
2827
|
+
const resolved = resolveExecutableOnPath(candidate.command);
|
|
2828
|
+
if (!resolved) {
|
|
2829
|
+
continue;
|
|
1099
2830
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
console.log(`Open this URL in your browser: ${url}`);
|
|
1103
|
-
return false;
|
|
2831
|
+
if (spawnDetached(resolved, candidate.args)) {
|
|
2832
|
+
return true;
|
|
1104
2833
|
}
|
|
1105
|
-
const child = spawn('xdg-open', [url], { detached: true, stdio: 'ignore' });
|
|
1106
|
-
child.on('error', () => {
|
|
1107
|
-
console.log(`Open this URL in your browser: ${url}`);
|
|
1108
|
-
});
|
|
1109
|
-
child.unref();
|
|
1110
|
-
return true;
|
|
1111
|
-
} catch {
|
|
1112
|
-
console.log(`Open this URL in your browser: ${url}`);
|
|
1113
|
-
return false;
|
|
1114
2834
|
}
|
|
2835
|
+
return false;
|
|
1115
2836
|
}
|
|
1116
2837
|
|
|
1117
2838
|
function handleCodexPreflightFailure(error) {
|
|
@@ -1131,7 +2852,7 @@ function handleCodexPreflightFailure(error) {
|
|
|
1131
2852
|
return true;
|
|
1132
2853
|
}
|
|
1133
2854
|
|
|
1134
|
-
function launchTui(url, questId, home,
|
|
2855
|
+
function launchTui(url, questId, home, runtimePython) {
|
|
1135
2856
|
const entry = ensureNodeBundle('src/tui', 'dist/index.js');
|
|
1136
2857
|
const args = [entry, '--base-url', url];
|
|
1137
2858
|
if (questId) {
|
|
@@ -1143,8 +2864,8 @@ function launchTui(url, questId, home, venvPython) {
|
|
|
1143
2864
|
env: {
|
|
1144
2865
|
...process.env,
|
|
1145
2866
|
DEEPSCIENTIST_TUI_HOME: home,
|
|
1146
|
-
DEEPSCIENTIST_TUI_PYTHON:
|
|
1147
|
-
|
|
2867
|
+
DEEPSCIENTIST_TUI_PYTHON: runtimePython,
|
|
2868
|
+
DEEPSCIENTIST_RUNTIME_PYTHON: runtimePython,
|
|
1148
2869
|
},
|
|
1149
2870
|
});
|
|
1150
2871
|
child.on('exit', (code) => {
|
|
@@ -1152,6 +2873,129 @@ function launchTui(url, questId, home, venvPython) {
|
|
|
1152
2873
|
});
|
|
1153
2874
|
}
|
|
1154
2875
|
|
|
2876
|
+
async function updateMain(rawArgs) {
|
|
2877
|
+
const options = parseUpdateArgs(rawArgs);
|
|
2878
|
+
if (!options) {
|
|
2879
|
+
printUpdateHelp();
|
|
2880
|
+
process.exit(1);
|
|
2881
|
+
}
|
|
2882
|
+
if (options.help) {
|
|
2883
|
+
printUpdateHelp();
|
|
2884
|
+
process.exit(0);
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
const home = options.home || resolveHome(rawArgs);
|
|
2888
|
+
ensureDir(home);
|
|
2889
|
+
|
|
2890
|
+
if (options.background && options.yes && !options.worker) {
|
|
2891
|
+
const payload = await startBackgroundUpdateWorker(home, {
|
|
2892
|
+
host: options.host,
|
|
2893
|
+
port: options.port,
|
|
2894
|
+
});
|
|
2895
|
+
if (options.json) {
|
|
2896
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
2897
|
+
} else {
|
|
2898
|
+
console.log(payload.message);
|
|
2899
|
+
}
|
|
2900
|
+
process.exit(payload.ok ? 0 : 1);
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
const status = checkForUpdates(home, { force: options.forceCheck || options.check || options.yes || options.worker });
|
|
2904
|
+
|
|
2905
|
+
if (options.remindLater) {
|
|
2906
|
+
const payload = markUpdateDeferred(home, status.latest_version);
|
|
2907
|
+
if (options.json) {
|
|
2908
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
2909
|
+
} else {
|
|
2910
|
+
console.log(`DeepScientist will remind you later about ${payload.latest_version || 'the next release'}.`);
|
|
2911
|
+
}
|
|
2912
|
+
process.exit(0);
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
if (options.skipVersion) {
|
|
2916
|
+
const payload = markUpdateSkipped(home, status.latest_version);
|
|
2917
|
+
if (options.json) {
|
|
2918
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
2919
|
+
} else {
|
|
2920
|
+
console.log(`DeepScientist will stop prompting for ${payload.last_skipped_version || payload.latest_version || 'this release'}.`);
|
|
2921
|
+
}
|
|
2922
|
+
process.exit(0);
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
if (options.worker) {
|
|
2926
|
+
const payload = await performSelfUpdate(home, {
|
|
2927
|
+
host: options.host,
|
|
2928
|
+
port: options.port,
|
|
2929
|
+
restartDaemon: options.restartDaemon,
|
|
2930
|
+
});
|
|
2931
|
+
if (options.json) {
|
|
2932
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
2933
|
+
} else {
|
|
2934
|
+
console.log(payload.message);
|
|
2935
|
+
}
|
|
2936
|
+
process.exit(payload.ok ? 0 : 1);
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
if (options.yes) {
|
|
2940
|
+
const payload = await performSelfUpdate(home, {
|
|
2941
|
+
host: options.host,
|
|
2942
|
+
port: options.port,
|
|
2943
|
+
restartDaemon: options.restartDaemon,
|
|
2944
|
+
});
|
|
2945
|
+
if (options.json) {
|
|
2946
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
2947
|
+
} else {
|
|
2948
|
+
console.log(payload.message);
|
|
2949
|
+
}
|
|
2950
|
+
process.exit(payload.ok ? 0 : 1);
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
if (options.check || options.json) {
|
|
2954
|
+
if (options.json) {
|
|
2955
|
+
console.log(JSON.stringify(status, null, 2));
|
|
2956
|
+
} else {
|
|
2957
|
+
printUpdateStatus(status);
|
|
2958
|
+
}
|
|
2959
|
+
process.exit(0);
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
if (!status.update_available) {
|
|
2963
|
+
printUpdateStatus(status, { compact: true });
|
|
2964
|
+
process.exit(0);
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2968
|
+
printUpdateStatus(status, { compact: true });
|
|
2969
|
+
process.exit(0);
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
const action = await promptUpdateAction(status);
|
|
2973
|
+
if (action === 'later') {
|
|
2974
|
+
markUpdateDeferred(home, status.latest_version);
|
|
2975
|
+
console.log('Update reminder deferred.');
|
|
2976
|
+
process.exit(0);
|
|
2977
|
+
}
|
|
2978
|
+
if (action === 'skip') {
|
|
2979
|
+
markUpdateSkipped(home, status.latest_version);
|
|
2980
|
+
console.log(`Skipped ${status.latest_version}.`);
|
|
2981
|
+
process.exit(0);
|
|
2982
|
+
}
|
|
2983
|
+
if (!status.can_self_update) {
|
|
2984
|
+
console.log(`Manual update command: ${status.manual_update_command}`);
|
|
2985
|
+
if (status.reason) {
|
|
2986
|
+
console.log(status.reason);
|
|
2987
|
+
}
|
|
2988
|
+
process.exit(0);
|
|
2989
|
+
}
|
|
2990
|
+
const payload = await performSelfUpdate(home, {
|
|
2991
|
+
host: options.host,
|
|
2992
|
+
port: options.port,
|
|
2993
|
+
restartDaemon: options.restartDaemon,
|
|
2994
|
+
});
|
|
2995
|
+
console.log(payload.message);
|
|
2996
|
+
process.exit(payload.ok ? 0 : 1);
|
|
2997
|
+
}
|
|
2998
|
+
|
|
1155
2999
|
async function launcherMain(rawArgs) {
|
|
1156
3000
|
const options = parseLauncherArgs(rawArgs);
|
|
1157
3001
|
if (!options) {
|
|
@@ -1195,11 +3039,23 @@ async function launcherMain(rawArgs) {
|
|
|
1195
3039
|
process.exit(healthy && (!state || identityMatch) ? 0 : 1);
|
|
1196
3040
|
}
|
|
1197
3041
|
|
|
1198
|
-
const
|
|
1199
|
-
|
|
3042
|
+
const pythonRuntime = ensurePythonRuntime(home);
|
|
3043
|
+
const runtimePython = pythonRuntime.runtimePython;
|
|
3044
|
+
ensureInitialized(home, runtimePython);
|
|
3045
|
+
if (await maybeHandleStartupUpdate(home, rawArgs, options)) {
|
|
3046
|
+
return true;
|
|
3047
|
+
}
|
|
1200
3048
|
maybePrintOptionalLatexNotice(home);
|
|
1201
3049
|
|
|
1202
|
-
const
|
|
3050
|
+
const configuredUi = await readConfiguredUiAddress(home, runtimePython, options.host, options.port);
|
|
3051
|
+
const host = configuredUi.host;
|
|
3052
|
+
const port = configuredUi.port;
|
|
3053
|
+
const mode = normalizeMode(options.mode ?? 'web');
|
|
3054
|
+
const shouldOpenBrowser = options.daemonOnly
|
|
3055
|
+
? false
|
|
3056
|
+
: options.openBrowser === null
|
|
3057
|
+
? configuredUi.autoOpenBrowser !== false && mode !== 'tui'
|
|
3058
|
+
: options.openBrowser;
|
|
1203
3059
|
if (options.restart) {
|
|
1204
3060
|
await stopDaemon(home);
|
|
1205
3061
|
}
|
|
@@ -1207,35 +3063,40 @@ async function launcherMain(rawArgs) {
|
|
|
1207
3063
|
step(4, 4, 'Starting local daemon and UI surfaces');
|
|
1208
3064
|
let started;
|
|
1209
3065
|
try {
|
|
1210
|
-
started = await startDaemon(home,
|
|
3066
|
+
started = await startDaemon(home, runtimePython, host, port);
|
|
1211
3067
|
} catch (error) {
|
|
1212
3068
|
if (handleCodexPreflightFailure(error)) return true;
|
|
1213
3069
|
throw error;
|
|
1214
3070
|
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
3071
|
+
const browserOpened = shouldOpenBrowser ? openBrowser(started.url) : false;
|
|
3072
|
+
printLaunchCard({
|
|
3073
|
+
url: started.url,
|
|
3074
|
+
bindUrl: started.bindUrl,
|
|
3075
|
+
mode,
|
|
3076
|
+
autoOpenRequested: shouldOpenBrowser,
|
|
3077
|
+
browserOpened,
|
|
3078
|
+
daemonOnly: options.daemonOnly,
|
|
3079
|
+
home,
|
|
3080
|
+
pythonSelection: pythonRuntime.runtimeProbe,
|
|
3081
|
+
});
|
|
1222
3082
|
|
|
1223
|
-
if (options.mode === 'web' || options.mode === 'both' || options.openBrowser) {
|
|
1224
|
-
openBrowser(started.url);
|
|
1225
|
-
}
|
|
1226
3083
|
if (options.daemonOnly) {
|
|
1227
3084
|
process.exit(0);
|
|
1228
3085
|
}
|
|
1229
|
-
if (
|
|
3086
|
+
if (mode === 'web') {
|
|
1230
3087
|
process.exit(0);
|
|
1231
3088
|
}
|
|
1232
|
-
launchTui(started.url, options.questId, home,
|
|
3089
|
+
launchTui(started.url, options.questId, home, runtimePython);
|
|
1233
3090
|
return true;
|
|
1234
3091
|
}
|
|
1235
3092
|
|
|
1236
3093
|
async function main() {
|
|
1237
3094
|
const args = process.argv.slice(2);
|
|
1238
3095
|
const positional = findFirstPositionalArg(args);
|
|
3096
|
+
if (positional && positional.value === 'update') {
|
|
3097
|
+
await updateMain(args);
|
|
3098
|
+
return;
|
|
3099
|
+
}
|
|
1239
3100
|
if (args.length === 0 || args[0] === 'ui' || (!positional && args[0]?.startsWith('--'))) {
|
|
1240
3101
|
await launcherMain(args);
|
|
1241
3102
|
return;
|
|
@@ -1246,15 +3107,16 @@ async function main() {
|
|
|
1246
3107
|
}
|
|
1247
3108
|
if (positional && pythonCommands.has(positional.value)) {
|
|
1248
3109
|
const home = resolveHome(args);
|
|
1249
|
-
const
|
|
3110
|
+
const pythonRuntime = ensurePythonRuntime(home);
|
|
3111
|
+
const runtimePython = pythonRuntime.runtimePython;
|
|
1250
3112
|
if (positional.value === 'run' || positional.value === 'daemon') {
|
|
1251
3113
|
maybePrintOptionalLatexNotice(home);
|
|
1252
3114
|
}
|
|
1253
3115
|
if (positional.value === 'run' || positional.value === 'daemon') {
|
|
1254
|
-
const bootstrapState = readCodexBootstrapState(home,
|
|
3116
|
+
const bootstrapState = readCodexBootstrapState(home, runtimePython);
|
|
1255
3117
|
if (!bootstrapState.codex_ready) {
|
|
1256
3118
|
try {
|
|
1257
|
-
const probe = probeCodexBootstrap(home,
|
|
3119
|
+
const probe = probeCodexBootstrap(home, runtimePython);
|
|
1258
3120
|
if (!probe || probe.ok !== true) {
|
|
1259
3121
|
throw createCodexPreflightError(home, probe);
|
|
1260
3122
|
}
|
|
@@ -1264,14 +3126,34 @@ async function main() {
|
|
|
1264
3126
|
}
|
|
1265
3127
|
}
|
|
1266
3128
|
}
|
|
1267
|
-
const result = runPythonCli(
|
|
3129
|
+
const result = runPythonCli(runtimePython, normalizePythonCliArgs(args, home), { allowFailure: true });
|
|
1268
3130
|
process.exit(result.status ?? 0);
|
|
1269
3131
|
return;
|
|
1270
3132
|
}
|
|
1271
3133
|
await launcherMain(args);
|
|
1272
3134
|
}
|
|
1273
3135
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
3136
|
+
module.exports = {
|
|
3137
|
+
__internal: {
|
|
3138
|
+
minimumPythonRequest,
|
|
3139
|
+
createPythonRuntimePlan,
|
|
3140
|
+
buildUvRuntimeEnv,
|
|
3141
|
+
runtimePythonEnvPath,
|
|
3142
|
+
runtimePythonPath,
|
|
3143
|
+
runtimeUvBinaryPath,
|
|
3144
|
+
legacyVenvRootPath,
|
|
3145
|
+
resolveUvBinary,
|
|
3146
|
+
resolveHome,
|
|
3147
|
+
useEditableProjectInstall,
|
|
3148
|
+
compareVersions,
|
|
3149
|
+
detectInstallMode,
|
|
3150
|
+
buildUpdateStatus,
|
|
3151
|
+
},
|
|
3152
|
+
};
|
|
3153
|
+
|
|
3154
|
+
if (require.main === module) {
|
|
3155
|
+
main().catch((error) => {
|
|
3156
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
3157
|
+
process.exit(1);
|
|
3158
|
+
});
|
|
3159
|
+
}
|