robot-resources 1.15.1 → 1.15.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -49
- package/README.md +0 -104
- package/bin/setup.js +0 -43
- package/lib/auth.mjs +0 -261
- package/lib/config.mjs +0 -55
- package/lib/detect.js +0 -254
- package/lib/health-report.js +0 -130
- package/lib/install-node-shim.js +0 -188
- package/lib/install-python-shim.js +0 -107
- package/lib/install-router-files.js +0 -48
- package/lib/json5.js +0 -16
- package/lib/login.mjs +0 -54
- package/lib/machine-id.js +0 -31
- package/lib/non-oc-wizard.js +0 -615
- package/lib/shell-config.js +0 -183
- package/lib/source-edit-attach.js +0 -469
- package/lib/tool-config.js +0 -504
- package/lib/ui.js +0 -87
- package/lib/uninstall.js +0 -204
- package/lib/venv-detect.js +0 -85
- package/lib/windows-env.js +0 -202
- package/lib/wizard.js +0 -523
package/lib/detect.js
DELETED
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { stripJson5 } from './json5.js';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Check if OpenClaw is installed.
|
|
8
|
-
*/
|
|
9
|
-
export function isOpenClawInstalled() {
|
|
10
|
-
const home = homedir();
|
|
11
|
-
return existsSync(join(home, '.openclaw')) || existsSync(join(home, 'openclaw.json'));
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Check if the Robot Resources OpenClaw plugin is already installed.
|
|
16
|
-
*/
|
|
17
|
-
export function isOpenClawPluginInstalled() {
|
|
18
|
-
const home = homedir();
|
|
19
|
-
const extDir = join(home, '.openclaw', 'extensions');
|
|
20
|
-
return existsSync(join(extDir, 'openclaw-plugin'))
|
|
21
|
-
|| existsSync(join(extDir, 'robot-resources-router'));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Check if the Robot Resources scraper OC plugin is already installed.
|
|
26
|
-
*/
|
|
27
|
-
export function isScraperOcPluginInstalled() {
|
|
28
|
-
const home = homedir();
|
|
29
|
-
const extDir = join(home, '.openclaw', 'extensions');
|
|
30
|
-
return existsSync(join(extDir, 'robot-resources-scraper-oc-plugin'));
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Detect OpenClaw auth mode: 'subscription' (OAuth token) or 'apikey'.
|
|
35
|
-
*
|
|
36
|
-
* Subscription users authenticate via Anthropic OAuth tokens.
|
|
37
|
-
* Anthropic rejects these tokens from third-party clients, so the
|
|
38
|
-
* only viable routing path is the OpenClaw plugin (not HTTP proxy).
|
|
39
|
-
*
|
|
40
|
-
* Detection order:
|
|
41
|
-
* 1. ANTHROPIC_AUTH_TOKEN env var → subscription
|
|
42
|
-
* 2. openclaw.json config:
|
|
43
|
-
* a. auth.type === 'oauth' | 'subscription' → subscription
|
|
44
|
-
* b. auth.profiles.*.mode === 'token' → subscription
|
|
45
|
-
* c. gateway.auth.mode === 'token' → subscription
|
|
46
|
-
* d. providers.anthropic.authToken → subscription
|
|
47
|
-
* e. providers.anthropic.apiKey → apikey
|
|
48
|
-
* 3. Default → apikey (conservative — proxy works fine)
|
|
49
|
-
*/
|
|
50
|
-
export function getOpenClawAuthMode() {
|
|
51
|
-
// Env var is the strongest signal
|
|
52
|
-
if (process.env.ANTHROPIC_AUTH_TOKEN) return 'subscription';
|
|
53
|
-
if (process.env.ANTHROPIC_API_KEY) return 'apikey';
|
|
54
|
-
|
|
55
|
-
// Try reading openclaw.json
|
|
56
|
-
const home = homedir();
|
|
57
|
-
const candidates = [
|
|
58
|
-
join(home, '.openclaw', 'openclaw.json'),
|
|
59
|
-
join(home, 'openclaw.json'),
|
|
60
|
-
];
|
|
61
|
-
|
|
62
|
-
for (const configPath of candidates) {
|
|
63
|
-
if (!existsSync(configPath)) continue;
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
const raw = readFileSync(configPath, 'utf-8');
|
|
67
|
-
const config = JSON.parse(stripJson5(raw));
|
|
68
|
-
|
|
69
|
-
// Check explicit auth type
|
|
70
|
-
if (config.auth?.type === 'oauth' || config.auth?.type === 'subscription') {
|
|
71
|
-
return 'subscription';
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Check auth profiles (real OC config: auth.profiles["anthropic:default"].mode)
|
|
75
|
-
const profiles = config.auth?.profiles;
|
|
76
|
-
if (profiles && typeof profiles === 'object') {
|
|
77
|
-
for (const profile of Object.values(profiles)) {
|
|
78
|
-
if (profile?.mode === 'token') return 'subscription';
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Check gateway auth mode
|
|
83
|
-
if (config.gateway?.auth?.mode === 'token') return 'subscription';
|
|
84
|
-
|
|
85
|
-
// Check for authToken in providers
|
|
86
|
-
const anthropic = config.models?.providers?.anthropic
|
|
87
|
-
|| config.providers?.anthropic;
|
|
88
|
-
if (anthropic?.authToken) return 'subscription';
|
|
89
|
-
if (anthropic?.apiKey) return 'apikey';
|
|
90
|
-
} catch {
|
|
91
|
-
// Config unreadable — fall through
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return 'apikey';
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Detect if the environment is headless (no browser available).
|
|
100
|
-
* On headless servers, login() tries xdg-open which fails silently,
|
|
101
|
-
* then hangs for 120s waiting for a callback that never comes.
|
|
102
|
-
*/
|
|
103
|
-
export function isHeadless() {
|
|
104
|
-
// SSH session — no local browser
|
|
105
|
-
if (process.env.SSH_CONNECTION || process.env.SSH_CLIENT) return true;
|
|
106
|
-
// Linux without a display server
|
|
107
|
-
if (process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return true;
|
|
108
|
-
// Docker / container
|
|
109
|
-
if (process.env.container || process.env.DOCKER_CONTAINER) return true;
|
|
110
|
-
return false;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Check if Claude Code is installed.
|
|
115
|
-
* Looks for ~/.claude/ directory which Claude Code creates on first run.
|
|
116
|
-
*/
|
|
117
|
-
export function isClaudeCodeInstalled() {
|
|
118
|
-
return existsSync(join(homedir(), '.claude'));
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Check if Cursor is installed.
|
|
123
|
-
* Looks for ~/.cursor/ directory which Cursor creates on first run.
|
|
124
|
-
*/
|
|
125
|
-
export function isCursorInstalled() {
|
|
126
|
-
return existsSync(join(homedir(), '.cursor'));
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// ── Agent-runtime detection (Phase 3) ─────────────────────────────────────
|
|
130
|
-
//
|
|
131
|
-
// Used by the non-OC wizard to pick which shim to install: a NODE_OPTIONS
|
|
132
|
-
// shell line for Node agents, or `pip install robot-resources` for Python.
|
|
133
|
-
// Returns { kind: 'node'|'python'|null, evidence: string[] } so the wizard
|
|
134
|
-
// can show the user WHY we picked a path (debuggable + builds trust).
|
|
135
|
-
//
|
|
136
|
-
// "Evidence" is the dep markers we found, in priority order. Empty string
|
|
137
|
-
// means a generic project (e.g. just package.json, no LLM SDK deps yet) —
|
|
138
|
-
// still picks the language but with low confidence.
|
|
139
|
-
|
|
140
|
-
// Exported (Phase 11.1) so the SDK-import scanner in source-edit-attach.js
|
|
141
|
-
// can reuse the same canonical list — single source of truth for "which npm
|
|
142
|
-
// packages count as an AI SDK signal."
|
|
143
|
-
export const NODE_AGENT_DEPS = [
|
|
144
|
-
'@anthropic-ai/sdk',
|
|
145
|
-
'openai',
|
|
146
|
-
'@google/generative-ai',
|
|
147
|
-
'@google-ai/generativelanguage',
|
|
148
|
-
'langchain',
|
|
149
|
-
'@langchain/core',
|
|
150
|
-
'@langchain/anthropic',
|
|
151
|
-
'@langchain/openai',
|
|
152
|
-
'@langchain/google-genai',
|
|
153
|
-
'@langchain/langgraph',
|
|
154
|
-
'@mastra/core',
|
|
155
|
-
'crewai-js',
|
|
156
|
-
'llamaindex',
|
|
157
|
-
'ai', // Vercel AI SDK
|
|
158
|
-
];
|
|
159
|
-
|
|
160
|
-
const PYTHON_AGENT_DEPS = [
|
|
161
|
-
'anthropic',
|
|
162
|
-
'openai',
|
|
163
|
-
'google-generativeai',
|
|
164
|
-
'langchain',
|
|
165
|
-
'langchain-anthropic',
|
|
166
|
-
'langchain-openai',
|
|
167
|
-
'langchain-google-genai',
|
|
168
|
-
'langgraph',
|
|
169
|
-
'crewai',
|
|
170
|
-
'llama-index',
|
|
171
|
-
'llama_index',
|
|
172
|
-
];
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Inspect cwd for evidence of a Node agent project. Returns null if no
|
|
176
|
-
* package.json, or `{ evidence: [...] }` describing matched dep markers
|
|
177
|
-
* (empty list means "Node project but no LLM-SDK deps detected" — generic).
|
|
178
|
-
*/
|
|
179
|
-
export function detectNodeAgent(cwd = process.cwd()) {
|
|
180
|
-
const pkgPath = join(cwd, 'package.json');
|
|
181
|
-
if (!existsSync(pkgPath)) return null;
|
|
182
|
-
|
|
183
|
-
try {
|
|
184
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
185
|
-
const allDeps = {
|
|
186
|
-
...(pkg.dependencies ?? {}),
|
|
187
|
-
...(pkg.devDependencies ?? {}),
|
|
188
|
-
...(pkg.peerDependencies ?? {}),
|
|
189
|
-
};
|
|
190
|
-
const evidence = NODE_AGENT_DEPS.filter(
|
|
191
|
-
(d) => Object.prototype.hasOwnProperty.call(allDeps, d),
|
|
192
|
-
);
|
|
193
|
-
return { evidence };
|
|
194
|
-
} catch {
|
|
195
|
-
// package.json unreadable but exists — call it Node, no evidence
|
|
196
|
-
return { evidence: [] };
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Inspect cwd for evidence of a Python agent project. Looks at
|
|
202
|
-
* requirements.txt + pyproject.toml. Returns null if neither exists, or
|
|
203
|
-
* `{ evidence: [...] }`. Empty evidence still resolves to Python — many
|
|
204
|
-
* agent projects use ad-hoc deps not in our markers list.
|
|
205
|
-
*/
|
|
206
|
-
export function detectPythonAgent(cwd = process.cwd()) {
|
|
207
|
-
const reqPath = join(cwd, 'requirements.txt');
|
|
208
|
-
const pyProjPath = join(cwd, 'pyproject.toml');
|
|
209
|
-
const hasReq = existsSync(reqPath);
|
|
210
|
-
const hasPy = existsSync(pyProjPath);
|
|
211
|
-
if (!hasReq && !hasPy) return null;
|
|
212
|
-
|
|
213
|
-
const text = [
|
|
214
|
-
hasReq ? safeRead(reqPath) : '',
|
|
215
|
-
hasPy ? safeRead(pyProjPath) : '',
|
|
216
|
-
].join('\n').toLowerCase();
|
|
217
|
-
|
|
218
|
-
const evidence = PYTHON_AGENT_DEPS.filter((d) => {
|
|
219
|
-
// Match `dep`, `dep==`, `dep>=`, `dep[extras]`, or `"dep"`/`'dep'` in
|
|
220
|
-
// pyproject's dependencies array. Word-boundary on the left, anything
|
|
221
|
-
// version-ish on the right.
|
|
222
|
-
const escaped = d.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
223
|
-
const re = new RegExp(`(^|[\\s"'\\[])${escaped}(\\s|[<>=!~\\["',]|$)`, 'm');
|
|
224
|
-
return re.test(text);
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
return { evidence };
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function safeRead(path) {
|
|
231
|
-
try { return readFileSync(path, 'utf-8'); } catch { return ''; }
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Decide which shim to install when OpenClaw is NOT detected. Picks Node OR
|
|
236
|
-
* Python based on cwd shape. When BOTH are present (full-stack monorepo
|
|
237
|
-
* with package.json AND pyproject.toml) the caller is responsible for
|
|
238
|
-
* resolving the ambiguity (interactively, or default to JS in --yes mode
|
|
239
|
-
* per the team decision in the plan).
|
|
240
|
-
*
|
|
241
|
-
* Returns one of:
|
|
242
|
-
* { kind: 'node', evidence: [...] }
|
|
243
|
-
* { kind: 'python', evidence: [...] }
|
|
244
|
-
* { kind: 'both', node: {...}, python: {...} }
|
|
245
|
-
* { kind: null } — unknown project shape, no clear path
|
|
246
|
-
*/
|
|
247
|
-
export function detectAgentRuntime(cwd = process.cwd()) {
|
|
248
|
-
const node = detectNodeAgent(cwd);
|
|
249
|
-
const python = detectPythonAgent(cwd);
|
|
250
|
-
if (node && python) return { kind: 'both', node, python };
|
|
251
|
-
if (node) return { kind: 'node', evidence: node.evidence };
|
|
252
|
-
if (python) return { kind: 'python', evidence: python.evidence };
|
|
253
|
-
return { kind: null };
|
|
254
|
-
}
|
package/lib/health-report.js
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import { readConfig } from './config.mjs';
|
|
5
|
-
|
|
6
|
-
const PROBE_TIMEOUT_MS = 5_000;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Run post-install health checks against all Robot Resources components.
|
|
10
|
-
*
|
|
11
|
-
* Probes:
|
|
12
|
-
* 1. Router — GET http://127.0.0.1:3838/health
|
|
13
|
-
* 2. Scraper — check openclaw.json for scraper MCP registration
|
|
14
|
-
* 3. Platform — GET {platformUrl}/v1/health with api_key
|
|
15
|
-
* 4. MCP — check openclaw.json for robot-resources-router registration
|
|
16
|
-
*
|
|
17
|
-
* @returns {{ status: 'healthy'|'partial'|'failed', components: Object, summary: string }}
|
|
18
|
-
*/
|
|
19
|
-
export async function checkHealth() {
|
|
20
|
-
const config = readConfig();
|
|
21
|
-
|
|
22
|
-
const [router, scraper, platform, mcp] = await Promise.all([
|
|
23
|
-
probeRouter(),
|
|
24
|
-
probeScraper(),
|
|
25
|
-
probePlatform(config),
|
|
26
|
-
probeMcp(),
|
|
27
|
-
]);
|
|
28
|
-
|
|
29
|
-
const components = { router, scraper, platform, mcp };
|
|
30
|
-
const healthyCount = Object.values(components).filter((c) => c.healthy).length;
|
|
31
|
-
const total = Object.keys(components).length;
|
|
32
|
-
|
|
33
|
-
let status;
|
|
34
|
-
if (healthyCount === total) {
|
|
35
|
-
status = 'healthy';
|
|
36
|
-
} else if (healthyCount === 0) {
|
|
37
|
-
status = 'failed';
|
|
38
|
-
} else {
|
|
39
|
-
status = 'partial';
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const failing = Object.entries(components)
|
|
43
|
-
.filter(([, c]) => !c.healthy)
|
|
44
|
-
.map(([name, c]) => `${name}: ${c.detail}`);
|
|
45
|
-
|
|
46
|
-
const summary =
|
|
47
|
-
status === 'healthy'
|
|
48
|
-
? `All ${total} components healthy.`
|
|
49
|
-
: `${healthyCount}/${total} healthy. Issues: ${failing.join('; ')}`;
|
|
50
|
-
|
|
51
|
-
return { status, components, summary };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function probeRouter() {
|
|
55
|
-
try {
|
|
56
|
-
if (typeof fetch === 'undefined') {
|
|
57
|
-
return { healthy: false, detail: 'fetch unavailable' };
|
|
58
|
-
}
|
|
59
|
-
const res = await fetch('http://127.0.0.1:3838/health', {
|
|
60
|
-
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
61
|
-
});
|
|
62
|
-
if (!res.ok) {
|
|
63
|
-
return { healthy: false, detail: `HTTP ${res.status}` };
|
|
64
|
-
}
|
|
65
|
-
const data = await res.json();
|
|
66
|
-
if (data.status === 'healthy' || data.status === 'degraded') {
|
|
67
|
-
return { healthy: true, detail: `running (v${data.version || 'unknown'})` };
|
|
68
|
-
}
|
|
69
|
-
return { healthy: false, detail: `status: ${data.status}` };
|
|
70
|
-
} catch (err) {
|
|
71
|
-
const detail = err.name === 'AbortError' ? 'timeout' : 'unreachable';
|
|
72
|
-
return { healthy: false, detail };
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function probeScraper() {
|
|
77
|
-
try {
|
|
78
|
-
const ocPath = join(homedir(), '.openclaw', 'openclaw.json');
|
|
79
|
-
const ocConfig = JSON.parse(readFileSync(ocPath, 'utf-8'));
|
|
80
|
-
const hasServer = !!ocConfig?.mcp?.servers?.['robot-resources-scraper'];
|
|
81
|
-
return {
|
|
82
|
-
healthy: hasServer,
|
|
83
|
-
detail: hasServer ? 'MCP registered' : 'scraper MCP not registered',
|
|
84
|
-
};
|
|
85
|
-
} catch {
|
|
86
|
-
return { healthy: false, detail: 'openclaw.json not found' };
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function probePlatform(config) {
|
|
91
|
-
if (!config.api_key) {
|
|
92
|
-
return { healthy: false, detail: 'no API key configured' };
|
|
93
|
-
}
|
|
94
|
-
try {
|
|
95
|
-
if (typeof fetch === 'undefined') {
|
|
96
|
-
return { healthy: false, detail: 'fetch unavailable' };
|
|
97
|
-
}
|
|
98
|
-
const platformUrl = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
|
|
99
|
-
const res = await fetch(`${platformUrl}/health`, {
|
|
100
|
-
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
101
|
-
});
|
|
102
|
-
return {
|
|
103
|
-
healthy: res.ok,
|
|
104
|
-
detail: res.ok ? 'reachable' : `HTTP ${res.status}`,
|
|
105
|
-
};
|
|
106
|
-
} catch (err) {
|
|
107
|
-
const detail = err.name === 'AbortError' ? 'timeout' : 'unreachable';
|
|
108
|
-
return { healthy: false, detail };
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function probeMcp() {
|
|
113
|
-
try {
|
|
114
|
-
const ocPath = join(homedir(), '.openclaw', 'openclaw.json');
|
|
115
|
-
const ocConfig = JSON.parse(readFileSync(ocPath, 'utf-8'));
|
|
116
|
-
const hasRouter = !!ocConfig?.plugins?.entries?.['robot-resources-router']?.enabled;
|
|
117
|
-
const hasScraper = !!ocConfig?.plugins?.entries?.['robot-resources-scraper-oc-plugin']?.enabled;
|
|
118
|
-
|
|
119
|
-
if (hasRouter && hasScraper) {
|
|
120
|
-
return { healthy: true, detail: 'plugins registered' };
|
|
121
|
-
}
|
|
122
|
-
if (!hasRouter && !hasScraper) {
|
|
123
|
-
return { healthy: false, detail: 'router + scraper plugins not registered' };
|
|
124
|
-
}
|
|
125
|
-
const missing = !hasRouter ? 'robot-resources-router' : 'robot-resources-scraper-oc-plugin';
|
|
126
|
-
return { healthy: false, detail: `${missing} not registered` };
|
|
127
|
-
} catch {
|
|
128
|
-
return { healthy: false, detail: 'openclaw.json not found' };
|
|
129
|
-
}
|
|
130
|
-
}
|
package/lib/install-node-shim.js
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import { writeShellLine, hasShellLine } from './shell-config.js';
|
|
2
|
-
import { readConfig } from './config.mjs';
|
|
3
|
-
import { detectNodeAgent } from './detect.js';
|
|
4
|
-
import { installRouterFiles } from './install-router-files.js';
|
|
5
|
-
import { writePersistedNodeOptions } from './windows-env.js';
|
|
6
|
-
|
|
7
|
-
const PLATFORM_URL = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Install the Node shim into the user's shell config (POSIX) or user
|
|
11
|
-
* environment registry (Windows).
|
|
12
|
-
*
|
|
13
|
-
* POSIX path (Phases 3 + 8):
|
|
14
|
-
* 1. Copy `@robot-resources/router` to ~/.robot-resources/router/
|
|
15
|
-
* (absolute path; survives cwd changes + npm/npx cache cleanup).
|
|
16
|
-
* 2. Append marker block with `--require <abs path>` to detected rc files
|
|
17
|
-
* (zsh / bash / fish).
|
|
18
|
-
*
|
|
19
|
-
* Windows path (Phase 9):
|
|
20
|
-
* 1. Same router-files copy — `homedir()` is platform-aware.
|
|
21
|
-
* 2. `setx NODE_OPTIONS "..."` writes to HKCU\\Environment so every new
|
|
22
|
-
* cmd / PowerShell / Win+R-launched Node process inherits it.
|
|
23
|
-
*
|
|
24
|
-
* Both paths emit `node_shim_installed` telemetry. The user has to open a
|
|
25
|
-
* new terminal for the change to take effect.
|
|
26
|
-
*
|
|
27
|
-
* Returns a UI-friendly result the wizard can format and print.
|
|
28
|
-
*/
|
|
29
|
-
export async function installNodeShim({ cwd = process.cwd(), dryRun = false } = {}) {
|
|
30
|
-
const sdks = detectSdks(cwd);
|
|
31
|
-
|
|
32
|
-
if (dryRun) {
|
|
33
|
-
await emit({
|
|
34
|
-
shell: 'dryrun',
|
|
35
|
-
shell_config_path: null,
|
|
36
|
-
sdks_detected: sdks,
|
|
37
|
-
dry_run: true,
|
|
38
|
-
reason: null,
|
|
39
|
-
});
|
|
40
|
-
return { ok: true, message: 'Dry-run: would have written NODE_OPTIONS.' };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Phase 8: copy router to an absolute path under ~/.robot-resources/router/
|
|
44
|
-
// before we wire the env config. If the copy fails, we don't write a
|
|
45
|
-
// broken NODE_OPTIONS line on either platform.
|
|
46
|
-
let autoPath;
|
|
47
|
-
try {
|
|
48
|
-
autoPath = installRouterFiles();
|
|
49
|
-
} catch (err) {
|
|
50
|
-
await emit({
|
|
51
|
-
shell: process.platform === 'win32' ? 'win32' : 'unknown',
|
|
52
|
-
shell_config_path: null,
|
|
53
|
-
sdks_detected: sdks,
|
|
54
|
-
dry_run: false,
|
|
55
|
-
reason: 'router_copy_failed',
|
|
56
|
-
error_messages: [err.message],
|
|
57
|
-
});
|
|
58
|
-
return {
|
|
59
|
-
ok: false,
|
|
60
|
-
message: `Could not copy router files to ~/.robot-resources/router/: ${err.message}`,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Windows branch — Phase 9. Mirrors the POSIX flow in shape: detect
|
|
65
|
-
// already-installed via the persisted registry value, write via setx,
|
|
66
|
-
// emit equivalent telemetry.
|
|
67
|
-
if (process.platform === 'win32') {
|
|
68
|
-
const winResult = writePersistedNodeOptions({ autoPath });
|
|
69
|
-
await emit({
|
|
70
|
-
shell: 'win32',
|
|
71
|
-
shell_config_path: 'HKCU\\Environment\\NODE_OPTIONS',
|
|
72
|
-
sdks_detected: sdks,
|
|
73
|
-
dry_run: false,
|
|
74
|
-
already_installed: !!winResult.already,
|
|
75
|
-
files_written: winResult.ok && !winResult.already ? 1 : 0,
|
|
76
|
-
files_with_errors: winResult.ok ? 0 : 1,
|
|
77
|
-
error_messages: winResult.ok ? [] : [winResult.error_message || winResult.reason || 'unknown'],
|
|
78
|
-
auto_path: autoPath,
|
|
79
|
-
win_node_options_length: winResult.length,
|
|
80
|
-
reason: winResult.ok ? null : winResult.reason,
|
|
81
|
-
});
|
|
82
|
-
if (winResult.ok && winResult.already) {
|
|
83
|
-
return {
|
|
84
|
-
ok: true,
|
|
85
|
-
already: true,
|
|
86
|
-
message: 'NODE_OPTIONS already includes the auto-attach line. No changes made.',
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
if (!winResult.ok) {
|
|
90
|
-
return {
|
|
91
|
-
ok: false,
|
|
92
|
-
reason: winResult.reason,
|
|
93
|
-
message: `Could not set NODE_OPTIONS via setx (${winResult.reason}): ${winResult.error_message || ''}`,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
return {
|
|
97
|
-
ok: true,
|
|
98
|
-
written: ['HKCU\\Environment\\NODE_OPTIONS'],
|
|
99
|
-
errors: [],
|
|
100
|
-
message:
|
|
101
|
-
'Set NODE_OPTIONS in your user environment (HKCU\\Environment). ' +
|
|
102
|
-
'Open a new terminal for it to take effect. Existing terminals will not see the change.',
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const alreadyInstalled = hasShellLine();
|
|
107
|
-
const result = writeShellLine({ autoPath });
|
|
108
|
-
|
|
109
|
-
// Single shell value for the funnel even though we may have written to
|
|
110
|
-
// multiple rc files. Pick the dominant one for telemetry.
|
|
111
|
-
const dominant = pickDominantShell(result.written);
|
|
112
|
-
|
|
113
|
-
await emit({
|
|
114
|
-
shell: dominant,
|
|
115
|
-
shell_config_path: result.written.join(','),
|
|
116
|
-
sdks_detected: sdks,
|
|
117
|
-
dry_run: false,
|
|
118
|
-
already_installed: alreadyInstalled,
|
|
119
|
-
files_written: result.written.length,
|
|
120
|
-
files_with_errors: result.errors.length,
|
|
121
|
-
error_messages: result.errors.map((e) => `${e.path}: ${e.message}`).slice(0, 3),
|
|
122
|
-
auto_path: autoPath,
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
if (alreadyInstalled && result.written.length === 0) {
|
|
126
|
-
return {
|
|
127
|
-
ok: true,
|
|
128
|
-
already: true,
|
|
129
|
-
message: 'NODE_OPTIONS auto-attach already installed. No changes made.',
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (result.written.length === 0 && result.errors.length > 0) {
|
|
134
|
-
return {
|
|
135
|
-
ok: false,
|
|
136
|
-
message: `Could not write to any shell rc file. Errors: ${result.errors.map((e) => e.message).join(', ')}`,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return {
|
|
141
|
-
ok: true,
|
|
142
|
-
written: result.written,
|
|
143
|
-
errors: result.errors,
|
|
144
|
-
message:
|
|
145
|
-
`Installed NODE_OPTIONS auto-attach in ${result.written.length} shell file(s). ` +
|
|
146
|
-
'Open a new terminal (or source the file) for it to take effect.',
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function detectSdks(cwd) {
|
|
151
|
-
const result = detectNodeAgent(cwd);
|
|
152
|
-
return result?.evidence ?? [];
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function pickDominantShell(paths) {
|
|
156
|
-
// Use process.env.SHELL as the tiebreaker — that's the user's actual
|
|
157
|
-
// login shell. Fall back to the first written file's basename.
|
|
158
|
-
const shellEnv = (process.env.SHELL || '').toLowerCase();
|
|
159
|
-
if (shellEnv.includes('zsh')) return 'zsh';
|
|
160
|
-
if (shellEnv.includes('fish')) return 'fish';
|
|
161
|
-
if (shellEnv.includes('bash')) return 'bash';
|
|
162
|
-
if (paths[0]?.endsWith('.zshrc')) return 'zsh';
|
|
163
|
-
if (paths[0]?.endsWith('.bashrc') || paths[0]?.endsWith('.bash_profile')) return 'bash';
|
|
164
|
-
if (paths[0]?.endsWith('config.fish')) return 'fish';
|
|
165
|
-
return 'unknown';
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
async function emit(payload) {
|
|
169
|
-
const config = readConfig();
|
|
170
|
-
if (!config.api_key) return;
|
|
171
|
-
try {
|
|
172
|
-
await fetch(`${PLATFORM_URL}/v1/telemetry`, {
|
|
173
|
-
method: 'POST',
|
|
174
|
-
headers: {
|
|
175
|
-
'Authorization': `Bearer ${config.api_key}`,
|
|
176
|
-
'Content-Type': 'application/json',
|
|
177
|
-
},
|
|
178
|
-
body: JSON.stringify({
|
|
179
|
-
product: 'cli',
|
|
180
|
-
event_type: 'node_shim_installed',
|
|
181
|
-
payload: { ...payload, platform: process.platform },
|
|
182
|
-
}),
|
|
183
|
-
signal: AbortSignal.timeout(5_000),
|
|
184
|
-
});
|
|
185
|
-
} catch {
|
|
186
|
-
// Best-effort — never let telemetry break the install path.
|
|
187
|
-
}
|
|
188
|
-
}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { detectVenv, runPipInstall } from './venv-detect.js';
|
|
3
|
-
import { readConfig } from './config.mjs';
|
|
4
|
-
import { detectPythonAgent } from './detect.js';
|
|
5
|
-
|
|
6
|
-
const PLATFORM_URL = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Install the Python shim into the user's active/cwd venv. Phase 3 entry
|
|
10
|
-
* for the non-OC Python path.
|
|
11
|
-
*
|
|
12
|
-
* Steps:
|
|
13
|
-
* 1. Resolve Python via detectVenv. Bail with `confidence: 'low'` if we
|
|
14
|
-
* can't find a venv — we never `pip install` into system Python (can
|
|
15
|
-
* break OS Python on Linux).
|
|
16
|
-
* 2. Run `pip install --upgrade robot-resources`. Captures exit code +
|
|
17
|
-
* stderr tail for telemetry.
|
|
18
|
-
* 3. Emit `python_shim_installed` telemetry with venv kind, Python
|
|
19
|
-
* version, detected SDK markers, pip exit code.
|
|
20
|
-
*
|
|
21
|
-
* Returns a UI-friendly result the wizard can format and print. Never
|
|
22
|
-
* throws — any unexpected error becomes a structured `{ ok: false, ... }`.
|
|
23
|
-
*/
|
|
24
|
-
export async function installPythonShim({ cwd = process.cwd() } = {}) {
|
|
25
|
-
const venv = detectVenv(cwd);
|
|
26
|
-
|
|
27
|
-
if (!venv.python) {
|
|
28
|
-
await emit({
|
|
29
|
-
kind: 'none',
|
|
30
|
-
python_version: null,
|
|
31
|
-
sdks_detected: detectSdks(cwd),
|
|
32
|
-
pip_exit_code: null,
|
|
33
|
-
reason: 'no_venv_found',
|
|
34
|
-
});
|
|
35
|
-
return {
|
|
36
|
-
ok: false,
|
|
37
|
-
reason: 'no_venv_found',
|
|
38
|
-
message:
|
|
39
|
-
'No active venv or ./.venv detected. Activate your venv first ' +
|
|
40
|
-
'(source .venv/bin/activate) or pass --python=/path/to/python, ' +
|
|
41
|
-
'then re-run.',
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const pipResult = runPipInstall({
|
|
46
|
-
python: venv.python,
|
|
47
|
-
packageSpec: 'robot-resources>=0.2.0',
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const pythonVersion = readPythonVersion(venv.python);
|
|
51
|
-
const sdks = detectSdks(cwd);
|
|
52
|
-
|
|
53
|
-
await emit({
|
|
54
|
-
kind: venv.kind,
|
|
55
|
-
python_version: pythonVersion,
|
|
56
|
-
sdks_detected: sdks,
|
|
57
|
-
pip_exit_code: pipResult.code,
|
|
58
|
-
pip_stderr_tail: pipResult.ok ? null : pipResult.stderr,
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
ok: pipResult.ok,
|
|
63
|
-
venv,
|
|
64
|
-
pythonVersion,
|
|
65
|
-
sdks,
|
|
66
|
-
pipResult,
|
|
67
|
-
message: pipResult.ok
|
|
68
|
-
? `Installed robot-resources into ${venv.kind} venv (${venv.python})`
|
|
69
|
-
: `pip install failed (exit ${pipResult.code}): ${pipResult.stderr.slice(0, 200)}`,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function detectSdks(cwd) {
|
|
74
|
-
const result = detectPythonAgent(cwd);
|
|
75
|
-
return result?.evidence ?? [];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function readPythonVersion(python) {
|
|
79
|
-
try {
|
|
80
|
-
const r = spawnSync(python, ['--version'], { encoding: 'utf-8' });
|
|
81
|
-
return (r.stdout || r.stderr || '').trim().replace(/^Python\s+/, '');
|
|
82
|
-
} catch {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async function emit(payload) {
|
|
88
|
-
const config = readConfig();
|
|
89
|
-
if (!config.api_key) return;
|
|
90
|
-
try {
|
|
91
|
-
await fetch(`${PLATFORM_URL}/v1/telemetry`, {
|
|
92
|
-
method: 'POST',
|
|
93
|
-
headers: {
|
|
94
|
-
'Authorization': `Bearer ${config.api_key}`,
|
|
95
|
-
'Content-Type': 'application/json',
|
|
96
|
-
},
|
|
97
|
-
body: JSON.stringify({
|
|
98
|
-
product: 'cli',
|
|
99
|
-
event_type: 'python_shim_installed',
|
|
100
|
-
payload: { ...payload, platform: process.platform },
|
|
101
|
-
}),
|
|
102
|
-
signal: AbortSignal.timeout(5_000),
|
|
103
|
-
});
|
|
104
|
-
} catch {
|
|
105
|
-
// Best-effort — never let telemetry break the install path.
|
|
106
|
-
}
|
|
107
|
-
}
|