@usejarvis/brain 0.1.0
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/LICENSE +153 -0
- package/README.md +278 -0
- package/bin/jarvis.ts +413 -0
- package/package.json +74 -0
- package/scripts/ensure-bun.cjs +8 -0
- package/src/actions/README.md +421 -0
- package/src/actions/app-control/desktop-controller.test.ts +26 -0
- package/src/actions/app-control/desktop-controller.ts +438 -0
- package/src/actions/app-control/interface.ts +64 -0
- package/src/actions/app-control/linux.ts +273 -0
- package/src/actions/app-control/macos.ts +54 -0
- package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
- package/src/actions/app-control/sidecar-launcher.ts +286 -0
- package/src/actions/app-control/windows.ts +44 -0
- package/src/actions/browser/cdp.ts +138 -0
- package/src/actions/browser/chrome-launcher.ts +252 -0
- package/src/actions/browser/session.ts +437 -0
- package/src/actions/browser/stealth.ts +49 -0
- package/src/actions/index.ts +20 -0
- package/src/actions/terminal/executor.ts +157 -0
- package/src/actions/terminal/wsl-bridge.ts +126 -0
- package/src/actions/test.ts +93 -0
- package/src/actions/tools/agents.ts +321 -0
- package/src/actions/tools/builtin.ts +846 -0
- package/src/actions/tools/commitments.ts +192 -0
- package/src/actions/tools/content.ts +217 -0
- package/src/actions/tools/delegate.ts +147 -0
- package/src/actions/tools/desktop.test.ts +55 -0
- package/src/actions/tools/desktop.ts +305 -0
- package/src/actions/tools/goals.ts +376 -0
- package/src/actions/tools/local-tools-guard.ts +20 -0
- package/src/actions/tools/registry.ts +171 -0
- package/src/actions/tools/research.ts +111 -0
- package/src/actions/tools/sidecar-list.ts +57 -0
- package/src/actions/tools/sidecar-route.ts +105 -0
- package/src/actions/tools/workflows.ts +216 -0
- package/src/agents/agent.ts +132 -0
- package/src/agents/delegation.ts +107 -0
- package/src/agents/hierarchy.ts +113 -0
- package/src/agents/index.ts +19 -0
- package/src/agents/messaging.ts +125 -0
- package/src/agents/orchestrator.ts +576 -0
- package/src/agents/role-discovery.ts +61 -0
- package/src/agents/sub-agent-runner.ts +307 -0
- package/src/agents/task-manager.ts +151 -0
- package/src/authority/approval-delivery.ts +59 -0
- package/src/authority/approval.ts +196 -0
- package/src/authority/audit.ts +158 -0
- package/src/authority/authority.test.ts +519 -0
- package/src/authority/deferred-executor.ts +103 -0
- package/src/authority/emergency.ts +66 -0
- package/src/authority/engine.ts +297 -0
- package/src/authority/index.ts +12 -0
- package/src/authority/learning.ts +111 -0
- package/src/authority/tool-action-map.ts +74 -0
- package/src/awareness/analytics.ts +466 -0
- package/src/awareness/awareness.test.ts +332 -0
- package/src/awareness/capture-engine.ts +305 -0
- package/src/awareness/context-graph.ts +130 -0
- package/src/awareness/context-tracker.ts +349 -0
- package/src/awareness/index.ts +25 -0
- package/src/awareness/intelligence.ts +321 -0
- package/src/awareness/ocr-engine.ts +88 -0
- package/src/awareness/service.ts +528 -0
- package/src/awareness/struggle-detector.ts +342 -0
- package/src/awareness/suggestion-engine.ts +476 -0
- package/src/awareness/types.ts +201 -0
- package/src/cli/autostart.ts +241 -0
- package/src/cli/deps.ts +449 -0
- package/src/cli/doctor.ts +230 -0
- package/src/cli/helpers.ts +401 -0
- package/src/cli/onboard.ts +580 -0
- package/src/comms/README.md +329 -0
- package/src/comms/auth-error.html +48 -0
- package/src/comms/channels/discord.ts +228 -0
- package/src/comms/channels/signal.ts +56 -0
- package/src/comms/channels/telegram.ts +316 -0
- package/src/comms/channels/whatsapp.ts +60 -0
- package/src/comms/channels.test.ts +173 -0
- package/src/comms/desktop-notify.ts +114 -0
- package/src/comms/example.ts +129 -0
- package/src/comms/index.ts +129 -0
- package/src/comms/streaming.ts +142 -0
- package/src/comms/voice.test.ts +152 -0
- package/src/comms/voice.ts +291 -0
- package/src/comms/websocket.test.ts +409 -0
- package/src/comms/websocket.ts +473 -0
- package/src/config/README.md +387 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.test.ts +137 -0
- package/src/config/loader.ts +142 -0
- package/src/config/types.ts +260 -0
- package/src/daemon/README.md +232 -0
- package/src/daemon/agent-service-interface.ts +9 -0
- package/src/daemon/agent-service.ts +600 -0
- package/src/daemon/api-routes.ts +2119 -0
- package/src/daemon/background-agent-service.ts +396 -0
- package/src/daemon/background-agent.test.ts +78 -0
- package/src/daemon/channel-service.ts +201 -0
- package/src/daemon/commitment-executor.ts +297 -0
- package/src/daemon/event-classifier.ts +239 -0
- package/src/daemon/event-coalescer.ts +123 -0
- package/src/daemon/event-reactor.ts +214 -0
- package/src/daemon/health.ts +220 -0
- package/src/daemon/index.ts +1004 -0
- package/src/daemon/llm-settings.ts +316 -0
- package/src/daemon/observer-service.ts +150 -0
- package/src/daemon/pid.ts +98 -0
- package/src/daemon/research-queue.ts +155 -0
- package/src/daemon/services.ts +175 -0
- package/src/daemon/ws-service.ts +788 -0
- package/src/goals/accountability.ts +240 -0
- package/src/goals/awareness-bridge.ts +185 -0
- package/src/goals/estimator.ts +185 -0
- package/src/goals/events.ts +28 -0
- package/src/goals/goals.test.ts +400 -0
- package/src/goals/integration.test.ts +329 -0
- package/src/goals/nl-builder.test.ts +220 -0
- package/src/goals/nl-builder.ts +256 -0
- package/src/goals/rhythm.test.ts +177 -0
- package/src/goals/rhythm.ts +275 -0
- package/src/goals/service.test.ts +135 -0
- package/src/goals/service.ts +348 -0
- package/src/goals/types.ts +106 -0
- package/src/goals/workflow-bridge.ts +96 -0
- package/src/integrations/google-api.ts +134 -0
- package/src/integrations/google-auth.ts +175 -0
- package/src/llm/README.md +291 -0
- package/src/llm/anthropic.ts +386 -0
- package/src/llm/gemini.ts +371 -0
- package/src/llm/index.ts +19 -0
- package/src/llm/manager.ts +153 -0
- package/src/llm/ollama.ts +307 -0
- package/src/llm/openai.ts +350 -0
- package/src/llm/provider.test.ts +231 -0
- package/src/llm/provider.ts +60 -0
- package/src/llm/test.ts +87 -0
- package/src/observers/README.md +278 -0
- package/src/observers/calendar.ts +113 -0
- package/src/observers/clipboard.ts +136 -0
- package/src/observers/email.ts +109 -0
- package/src/observers/example.ts +58 -0
- package/src/observers/file-watcher.ts +124 -0
- package/src/observers/index.ts +159 -0
- package/src/observers/notifications.ts +197 -0
- package/src/observers/observers.test.ts +203 -0
- package/src/observers/processes.ts +225 -0
- package/src/personality/README.md +61 -0
- package/src/personality/adapter.ts +196 -0
- package/src/personality/index.ts +20 -0
- package/src/personality/learner.ts +209 -0
- package/src/personality/model.ts +132 -0
- package/src/personality/personality.test.ts +236 -0
- package/src/roles/README.md +252 -0
- package/src/roles/authority.ts +119 -0
- package/src/roles/example-usage.ts +198 -0
- package/src/roles/index.ts +42 -0
- package/src/roles/loader.ts +143 -0
- package/src/roles/prompt-builder.ts +194 -0
- package/src/roles/test-multi.ts +102 -0
- package/src/roles/test-role.yaml +77 -0
- package/src/roles/test-utils.ts +93 -0
- package/src/roles/test.ts +106 -0
- package/src/roles/tool-guide.ts +190 -0
- package/src/roles/types.ts +36 -0
- package/src/roles/utils.ts +200 -0
- package/src/scripts/google-setup.ts +168 -0
- package/src/sidecar/connection.ts +179 -0
- package/src/sidecar/index.ts +6 -0
- package/src/sidecar/manager.ts +542 -0
- package/src/sidecar/protocol.ts +85 -0
- package/src/sidecar/rpc.ts +161 -0
- package/src/sidecar/scheduler.ts +136 -0
- package/src/sidecar/types.ts +112 -0
- package/src/sidecar/validator.ts +144 -0
- package/src/vault/README.md +110 -0
- package/src/vault/awareness.ts +341 -0
- package/src/vault/commitments.ts +299 -0
- package/src/vault/content-pipeline.ts +260 -0
- package/src/vault/conversations.ts +173 -0
- package/src/vault/entities.ts +180 -0
- package/src/vault/extractor.test.ts +356 -0
- package/src/vault/extractor.ts +345 -0
- package/src/vault/facts.ts +190 -0
- package/src/vault/goals.ts +477 -0
- package/src/vault/index.ts +87 -0
- package/src/vault/keychain.ts +99 -0
- package/src/vault/observations.ts +115 -0
- package/src/vault/relationships.ts +178 -0
- package/src/vault/retrieval.test.ts +126 -0
- package/src/vault/retrieval.ts +227 -0
- package/src/vault/schema.ts +658 -0
- package/src/vault/settings.ts +38 -0
- package/src/vault/vectors.ts +92 -0
- package/src/vault/workflows.ts +403 -0
- package/src/workflows/auto-suggest.ts +290 -0
- package/src/workflows/engine.ts +366 -0
- package/src/workflows/events.ts +24 -0
- package/src/workflows/executor.ts +207 -0
- package/src/workflows/nl-builder.ts +198 -0
- package/src/workflows/nodes/actions/agent-task.ts +73 -0
- package/src/workflows/nodes/actions/calendar-action.ts +85 -0
- package/src/workflows/nodes/actions/code-execution.ts +73 -0
- package/src/workflows/nodes/actions/discord.ts +77 -0
- package/src/workflows/nodes/actions/file-write.ts +73 -0
- package/src/workflows/nodes/actions/gmail.ts +69 -0
- package/src/workflows/nodes/actions/http-request.ts +117 -0
- package/src/workflows/nodes/actions/notification.ts +85 -0
- package/src/workflows/nodes/actions/run-tool.ts +55 -0
- package/src/workflows/nodes/actions/send-message.ts +82 -0
- package/src/workflows/nodes/actions/shell-command.ts +76 -0
- package/src/workflows/nodes/actions/telegram.ts +60 -0
- package/src/workflows/nodes/builtin.ts +119 -0
- package/src/workflows/nodes/error/error-handler.ts +37 -0
- package/src/workflows/nodes/error/fallback.ts +47 -0
- package/src/workflows/nodes/error/retry.ts +82 -0
- package/src/workflows/nodes/logic/delay.ts +42 -0
- package/src/workflows/nodes/logic/if-else.ts +41 -0
- package/src/workflows/nodes/logic/loop.ts +90 -0
- package/src/workflows/nodes/logic/merge.ts +38 -0
- package/src/workflows/nodes/logic/race.ts +40 -0
- package/src/workflows/nodes/logic/switch.ts +59 -0
- package/src/workflows/nodes/logic/template-render.ts +53 -0
- package/src/workflows/nodes/logic/variable-get.ts +37 -0
- package/src/workflows/nodes/logic/variable-set.ts +59 -0
- package/src/workflows/nodes/registry.ts +99 -0
- package/src/workflows/nodes/transform/aggregate.ts +99 -0
- package/src/workflows/nodes/transform/csv-parse.ts +70 -0
- package/src/workflows/nodes/transform/json-parse.ts +63 -0
- package/src/workflows/nodes/transform/map-filter.ts +84 -0
- package/src/workflows/nodes/transform/regex-match.ts +89 -0
- package/src/workflows/nodes/triggers/calendar.ts +33 -0
- package/src/workflows/nodes/triggers/clipboard.ts +32 -0
- package/src/workflows/nodes/triggers/cron.ts +40 -0
- package/src/workflows/nodes/triggers/email.ts +40 -0
- package/src/workflows/nodes/triggers/file-change.ts +45 -0
- package/src/workflows/nodes/triggers/git.ts +46 -0
- package/src/workflows/nodes/triggers/manual.ts +23 -0
- package/src/workflows/nodes/triggers/poll.ts +81 -0
- package/src/workflows/nodes/triggers/process.ts +44 -0
- package/src/workflows/nodes/triggers/screen-event.ts +37 -0
- package/src/workflows/nodes/triggers/webhook.ts +39 -0
- package/src/workflows/safe-eval.ts +139 -0
- package/src/workflows/template.ts +118 -0
- package/src/workflows/triggers/cron.ts +311 -0
- package/src/workflows/triggers/manager.ts +285 -0
- package/src/workflows/triggers/observer-bridge.ts +172 -0
- package/src/workflows/triggers/poller.ts +201 -0
- package/src/workflows/triggers/screen-condition.ts +218 -0
- package/src/workflows/triggers/triggers.test.ts +740 -0
- package/src/workflows/triggers/webhook.ts +191 -0
- package/src/workflows/types.ts +133 -0
- package/src/workflows/variables.ts +72 -0
- package/src/workflows/workflows.test.ts +383 -0
- package/src/workflows/yaml.ts +104 -0
- package/ui/dist/index-j75njzc1.css +1199 -0
- package/ui/dist/index-p2zh407q.js +80603 -0
- package/ui/dist/index.html +13 -0
- package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
- package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
- package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
- package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
- package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* J.A.R.V.I.S. Doctor — Environment Diagnostics
|
|
3
|
+
*
|
|
4
|
+
* Checks system requirements, configuration, and connectivity.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
10
|
+
import {
|
|
11
|
+
c, printBanner, printOk, printWarn, printErr, printInfo, startSpinner, closeRL,
|
|
12
|
+
} from './helpers.ts';
|
|
13
|
+
|
|
14
|
+
const JARVIS_DIR = join(homedir(), '.jarvis');
|
|
15
|
+
const CONFIG_PATH = join(JARVIS_DIR, 'config.yaml');
|
|
16
|
+
|
|
17
|
+
interface CheckResult {
|
|
18
|
+
name: string;
|
|
19
|
+
status: 'ok' | 'warn' | 'fail' | 'skip';
|
|
20
|
+
message: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function runDoctor(): Promise<void> {
|
|
24
|
+
printBanner();
|
|
25
|
+
console.log(c.bold('Running system diagnostics...\n'));
|
|
26
|
+
|
|
27
|
+
const results: CheckResult[] = [];
|
|
28
|
+
|
|
29
|
+
// ── Check 1: Bun Version ──────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const bunVersion = Bun.version;
|
|
32
|
+
const [major] = bunVersion.split('.').map(Number);
|
|
33
|
+
if (major! >= 1) {
|
|
34
|
+
results.push({ name: 'Bun Runtime', status: 'ok', message: `v${bunVersion}` });
|
|
35
|
+
} else {
|
|
36
|
+
results.push({ name: 'Bun Runtime', status: 'warn', message: `v${bunVersion} (>= 1.0.0 recommended)` });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Check 2: Data Directory ───────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
if (existsSync(JARVIS_DIR)) {
|
|
42
|
+
results.push({ name: 'Data Directory', status: 'ok', message: JARVIS_DIR });
|
|
43
|
+
} else {
|
|
44
|
+
results.push({ name: 'Data Directory', status: 'warn', message: `${JARVIS_DIR} not found. Run: jarvis onboard` });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Check 3: Config File ──────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
let config: any = null;
|
|
50
|
+
if (existsSync(CONFIG_PATH)) {
|
|
51
|
+
try {
|
|
52
|
+
const YAML = (await import('yaml')).default;
|
|
53
|
+
const text = readFileSync(CONFIG_PATH, 'utf-8');
|
|
54
|
+
config = YAML.parse(text);
|
|
55
|
+
results.push({ name: 'Config File', status: 'ok', message: CONFIG_PATH });
|
|
56
|
+
} catch (err) {
|
|
57
|
+
results.push({ name: 'Config File', status: 'fail', message: `Invalid YAML: ${err}` });
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
results.push({ name: 'Config File', status: 'fail', message: 'Not found. Run: jarvis onboard' });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Check 4: LLM API Key ─────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
if (config) {
|
|
66
|
+
const primary = config.llm?.primary ?? 'anthropic';
|
|
67
|
+
const providerConfig = config.llm?.[primary];
|
|
68
|
+
|
|
69
|
+
if (primary === 'ollama') {
|
|
70
|
+
results.push({ name: 'LLM Provider', status: 'ok', message: `ollama (${providerConfig?.model ?? 'llama3'})` });
|
|
71
|
+
} else if (providerConfig?.api_key && providerConfig.api_key !== '') {
|
|
72
|
+
results.push({
|
|
73
|
+
name: 'LLM Provider',
|
|
74
|
+
status: 'ok',
|
|
75
|
+
message: `${primary} (key: ${providerConfig.api_key.slice(0, 10)}...)`,
|
|
76
|
+
});
|
|
77
|
+
} else {
|
|
78
|
+
results.push({ name: 'LLM Provider', status: 'fail', message: `${primary} API key not set` });
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
results.push({ name: 'LLM Provider', status: 'skip', message: 'No config file' });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Check 5: LLM Connectivity ────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
if (config) {
|
|
87
|
+
const spin = startSpinner('Testing LLM connectivity...');
|
|
88
|
+
try {
|
|
89
|
+
const { LLMManager, AnthropicProvider, OpenAIProvider, GeminiProvider, OllamaProvider } = await import('../llm/index.ts');
|
|
90
|
+
const manager = new LLMManager();
|
|
91
|
+
const primary = config.llm?.primary ?? 'anthropic';
|
|
92
|
+
|
|
93
|
+
if (primary === 'anthropic' && config.llm?.anthropic?.api_key) {
|
|
94
|
+
manager.registerProvider(new AnthropicProvider(config.llm.anthropic.api_key, config.llm.anthropic.model));
|
|
95
|
+
} else if (primary === 'openai' && config.llm?.openai?.api_key) {
|
|
96
|
+
manager.registerProvider(new OpenAIProvider(config.llm.openai.api_key, config.llm.openai.model));
|
|
97
|
+
} else if (primary === 'gemini' && config.llm?.gemini?.api_key) {
|
|
98
|
+
manager.registerProvider(new GeminiProvider(config.llm.gemini.api_key, config.llm.gemini.model));
|
|
99
|
+
} else if (primary === 'ollama') {
|
|
100
|
+
manager.registerProvider(new OllamaProvider(config.llm.ollama?.base_url, config.llm.ollama?.model));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
manager.setPrimary(primary);
|
|
104
|
+
const resp = await manager.chat(
|
|
105
|
+
[{ role: 'user', content: 'Say OK' }],
|
|
106
|
+
{ max_tokens: 10 },
|
|
107
|
+
);
|
|
108
|
+
spin.stop();
|
|
109
|
+
results.push({ name: 'LLM Connectivity', status: 'ok', message: `Model: ${resp.model}` });
|
|
110
|
+
} catch (err) {
|
|
111
|
+
spin.stop();
|
|
112
|
+
results.push({ name: 'LLM Connectivity', status: 'fail', message: String(err).slice(0, 100) });
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
results.push({ name: 'LLM Connectivity', status: 'skip', message: 'No config file' });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Check 6: Browser (Chromium/Chrome) ────────────────────────────
|
|
119
|
+
|
|
120
|
+
const browserPaths = process.platform === 'darwin'
|
|
121
|
+
? ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/Applications/Chromium.app/Contents/MacOS/Chromium']
|
|
122
|
+
: ['/usr/bin/chromium-browser', '/usr/bin/chromium', '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable'];
|
|
123
|
+
|
|
124
|
+
const foundBrowser = browserPaths.find(p => existsSync(p));
|
|
125
|
+
if (foundBrowser) {
|
|
126
|
+
results.push({ name: 'Browser', status: 'ok', message: foundBrowser });
|
|
127
|
+
} else {
|
|
128
|
+
// Try which
|
|
129
|
+
const which = Bun.spawnSync(['which', 'chromium-browser']);
|
|
130
|
+
if (which.exitCode === 0) {
|
|
131
|
+
results.push({ name: 'Browser', status: 'ok', message: which.stdout.toString().trim() });
|
|
132
|
+
} else {
|
|
133
|
+
results.push({ name: 'Browser', status: 'warn', message: 'Chromium/Chrome not found. Browser tools will be limited.' });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── Check 7: Port Availability ────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
const port = config?.daemon?.port ?? 3142;
|
|
140
|
+
try {
|
|
141
|
+
const server = Bun.serve({ port, fetch: () => new Response('') });
|
|
142
|
+
server.stop(true);
|
|
143
|
+
results.push({ name: 'Port', status: 'ok', message: `${port} is available` });
|
|
144
|
+
} catch {
|
|
145
|
+
results.push({ name: 'Port', status: 'warn', message: `${port} is in use (daemon may already be running)` });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Check 8: SQLite ───────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const { Database } = await import('bun:sqlite');
|
|
152
|
+
const db = new Database(':memory:');
|
|
153
|
+
db.run('CREATE TABLE test (id INTEGER PRIMARY KEY)');
|
|
154
|
+
db.close();
|
|
155
|
+
results.push({ name: 'SQLite', status: 'ok', message: 'bun:sqlite working' });
|
|
156
|
+
} catch (err) {
|
|
157
|
+
results.push({ name: 'SQLite', status: 'fail', message: String(err) });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── Check 9: TTS/STT Providers ────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
if (config?.tts?.enabled) {
|
|
163
|
+
results.push({ name: 'TTS', status: 'ok', message: `${config.tts.provider ?? 'edge'} (${config.tts.voice ?? 'default'})` });
|
|
164
|
+
} else {
|
|
165
|
+
results.push({ name: 'TTS', status: 'skip', message: 'Disabled' });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (config?.stt?.provider) {
|
|
169
|
+
const sttProv = config.stt.provider;
|
|
170
|
+
const hasKey = sttProv === 'ollama' || sttProv === 'local'
|
|
171
|
+
|| (sttProv === 'openai' && config.stt.openai?.api_key)
|
|
172
|
+
|| (sttProv === 'groq' && config.stt.groq?.api_key);
|
|
173
|
+
results.push({
|
|
174
|
+
name: 'STT',
|
|
175
|
+
status: hasKey ? 'ok' : 'warn',
|
|
176
|
+
message: hasKey ? `${sttProv} configured` : `${sttProv} (API key missing)`,
|
|
177
|
+
});
|
|
178
|
+
} else {
|
|
179
|
+
results.push({ name: 'STT', status: 'skip', message: 'Not configured' });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── Check 11: Channels ────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
if (config?.channels?.telegram?.enabled && config.channels.telegram.bot_token) {
|
|
185
|
+
results.push({ name: 'Telegram', status: 'ok', message: 'Bot token set' });
|
|
186
|
+
} else {
|
|
187
|
+
results.push({ name: 'Telegram', status: 'skip', message: 'Not configured' });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (config?.channels?.discord?.enabled && config.channels.discord.bot_token) {
|
|
191
|
+
results.push({ name: 'Discord', status: 'ok', message: 'Bot token set' });
|
|
192
|
+
} else {
|
|
193
|
+
results.push({ name: 'Discord', status: 'skip', message: 'Not configured' });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── Results ───────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
console.log(c.bold('\nDiagnostics Results:\n'));
|
|
199
|
+
|
|
200
|
+
let okCount = 0;
|
|
201
|
+
let warnCount = 0;
|
|
202
|
+
let failCount = 0;
|
|
203
|
+
|
|
204
|
+
for (const r of results) {
|
|
205
|
+
const icon = r.status === 'ok' ? c.green('✓')
|
|
206
|
+
: r.status === 'warn' ? c.yellow('!')
|
|
207
|
+
: r.status === 'fail' ? c.red('✗')
|
|
208
|
+
: c.dim('○');
|
|
209
|
+
|
|
210
|
+
const nameStr = r.name.padEnd(20);
|
|
211
|
+
console.log(` ${icon} ${nameStr} ${c.dim(r.message)}`);
|
|
212
|
+
|
|
213
|
+
if (r.status === 'ok') okCount++;
|
|
214
|
+
else if (r.status === 'warn') warnCount++;
|
|
215
|
+
else if (r.status === 'fail') failCount++;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
console.log('');
|
|
219
|
+
console.log(` ${c.green(`${okCount} passed`)} ${c.yellow(`${warnCount} warnings`)} ${c.red(`${failCount} failed`)}`);
|
|
220
|
+
|
|
221
|
+
if (failCount > 0) {
|
|
222
|
+
console.log(c.red('\nSome checks failed. Run "jarvis onboard" to fix configuration.\n'));
|
|
223
|
+
} else if (warnCount > 0) {
|
|
224
|
+
console.log(c.yellow('\nAll critical checks passed, but some optional features need setup.\n'));
|
|
225
|
+
} else {
|
|
226
|
+
console.log(c.green('\nAll checks passed! JARVIS is ready.\n'));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
closeRL();
|
|
230
|
+
}
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Utilities for J.A.R.V.I.S.
|
|
3
|
+
*
|
|
4
|
+
* Shared helpers for the interactive CLI: prompts, colors, spinners.
|
|
5
|
+
* Zero external dependencies — uses built-in readline and ANSI codes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as readline from 'node:readline';
|
|
9
|
+
import { createReadStream, openSync, closeSync, readFileSync, existsSync } from 'node:fs';
|
|
10
|
+
|
|
11
|
+
// ── ANSI Colors ──────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const RESET = '\x1b[0m';
|
|
14
|
+
const BOLD = '\x1b[1m';
|
|
15
|
+
const DIM = '\x1b[2m';
|
|
16
|
+
const CYAN = '\x1b[36m';
|
|
17
|
+
const GREEN = '\x1b[32m';
|
|
18
|
+
const YELLOW = '\x1b[33m';
|
|
19
|
+
const RED = '\x1b[31m';
|
|
20
|
+
const MAGENTA = '\x1b[35m';
|
|
21
|
+
const BLUE = '\x1b[34m';
|
|
22
|
+
|
|
23
|
+
export const c = {
|
|
24
|
+
bold: (s: string) => `${BOLD}${s}${RESET}`,
|
|
25
|
+
dim: (s: string) => `${DIM}${s}${RESET}`,
|
|
26
|
+
cyan: (s: string) => `${CYAN}${s}${RESET}`,
|
|
27
|
+
green: (s: string) => `${GREEN}${s}${RESET}`,
|
|
28
|
+
yellow: (s: string) => `${YELLOW}${s}${RESET}`,
|
|
29
|
+
red: (s: string) => `${RED}${s}${RESET}`,
|
|
30
|
+
magenta: (s: string) => `${MAGENTA}${s}${RESET}`,
|
|
31
|
+
blue: (s: string) => `${BLUE}${s}${RESET}`,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// ── Readline Interface ───────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
let rl: readline.Interface | null = null;
|
|
37
|
+
let ttyStream: ReturnType<typeof createReadStream> | null = null;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get a readline interface connected to the terminal.
|
|
41
|
+
* When stdin is a pipe (e.g. `curl | bash`), falls back to /dev/tty.
|
|
42
|
+
*/
|
|
43
|
+
function getRL(): readline.Interface {
|
|
44
|
+
if (!rl) {
|
|
45
|
+
let input: NodeJS.ReadableStream = process.stdin;
|
|
46
|
+
|
|
47
|
+
// When stdin is not a TTY (piped), open /dev/tty directly
|
|
48
|
+
if (!process.stdin.isTTY) {
|
|
49
|
+
try {
|
|
50
|
+
// Test that /dev/tty is actually openable (fails in headless/CI)
|
|
51
|
+
const fd = openSync('/dev/tty', 'r');
|
|
52
|
+
closeSync(fd);
|
|
53
|
+
ttyStream = createReadStream('/dev/tty');
|
|
54
|
+
input = ttyStream;
|
|
55
|
+
} catch {
|
|
56
|
+
// No controlling terminal available — fall back to stdin
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
rl = readline.createInterface({ input, output: process.stdout });
|
|
61
|
+
|
|
62
|
+
// If the readline closes unexpectedly (EOF), null it so the next
|
|
63
|
+
// call to getRL() creates a fresh one from /dev/tty.
|
|
64
|
+
rl.on('close', () => { rl = null; });
|
|
65
|
+
}
|
|
66
|
+
return rl;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function closeRL(): void {
|
|
70
|
+
if (rl) {
|
|
71
|
+
rl.close();
|
|
72
|
+
rl = null;
|
|
73
|
+
}
|
|
74
|
+
if (ttyStream) {
|
|
75
|
+
ttyStream.close();
|
|
76
|
+
ttyStream = null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Prompt Helpers ───────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Ask a free-text question. Returns trimmed answer.
|
|
84
|
+
*/
|
|
85
|
+
export function ask(question: string, defaultValue?: string): Promise<string> {
|
|
86
|
+
return new Promise((resolve) => {
|
|
87
|
+
const suffix = defaultValue ? ` ${c.dim(`[${defaultValue}]`)}` : '';
|
|
88
|
+
getRL().question(`${c.cyan('?')} ${question}${suffix}: `, (answer) => {
|
|
89
|
+
resolve(answer.trim() || defaultValue || '');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Ask for a secret (API key, token). Masks input with asterisks.
|
|
96
|
+
*/
|
|
97
|
+
export function askSecret(question: string): Promise<string> {
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
const r = getRL();
|
|
100
|
+
process.stdout.write(`${c.cyan('?')} ${question}: `);
|
|
101
|
+
|
|
102
|
+
// Temporarily disable echo
|
|
103
|
+
const stdin = process.stdin;
|
|
104
|
+
const wasRaw = stdin.isRaw;
|
|
105
|
+
if (stdin.isTTY && stdin.setRawMode) {
|
|
106
|
+
stdin.setRawMode(true);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let input = '';
|
|
110
|
+
|
|
111
|
+
const cleanup = () => {
|
|
112
|
+
stdin.removeListener('data', onData);
|
|
113
|
+
if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(wasRaw ?? false);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const onData = (char: Buffer) => {
|
|
117
|
+
try {
|
|
118
|
+
const ch = char.toString();
|
|
119
|
+
if (ch === '\n' || ch === '\r') {
|
|
120
|
+
cleanup();
|
|
121
|
+
process.stdout.write('\n');
|
|
122
|
+
resolve(input.trim());
|
|
123
|
+
} else if (ch === '\x7f' || ch === '\b') {
|
|
124
|
+
if (input.length > 0) {
|
|
125
|
+
input = input.slice(0, -1);
|
|
126
|
+
process.stdout.write('\b \b');
|
|
127
|
+
}
|
|
128
|
+
} else if (ch === '\x03') {
|
|
129
|
+
cleanup();
|
|
130
|
+
process.stdout.write('\n');
|
|
131
|
+
process.exit(130);
|
|
132
|
+
} else if (ch.charCodeAt(0) >= 32) {
|
|
133
|
+
input += ch;
|
|
134
|
+
process.stdout.write('*');
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
cleanup();
|
|
138
|
+
process.stdout.write('\n');
|
|
139
|
+
resolve(input.trim());
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
stdin.on('data', onData);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Ask a yes/no question. Returns boolean.
|
|
149
|
+
*/
|
|
150
|
+
export function askYesNo(question: string, defaultYes = true): Promise<boolean> {
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
const hint = defaultYes ? 'Y/n' : 'y/N';
|
|
153
|
+
getRL().question(`${c.cyan('?')} ${question} ${c.dim(`(${hint})`)}: `, (answer) => {
|
|
154
|
+
const a = answer.trim().toLowerCase();
|
|
155
|
+
if (a === '') resolve(defaultYes);
|
|
156
|
+
else resolve(a === 'y' || a === 'yes');
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Ask user to pick from a list of options.
|
|
163
|
+
* Supports arrow keys (up/down) and number keys for selection.
|
|
164
|
+
* Falls back to simple numbered input when raw mode is unavailable.
|
|
165
|
+
*/
|
|
166
|
+
export function askChoice<T extends string>(
|
|
167
|
+
question: string,
|
|
168
|
+
options: { label: string; value: T; description?: string }[],
|
|
169
|
+
defaultValue?: T
|
|
170
|
+
): Promise<T> {
|
|
171
|
+
let selected = 0;
|
|
172
|
+
options.forEach((opt, i) => {
|
|
173
|
+
if (opt.value === defaultValue) selected = i;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const stdin = process.stdin;
|
|
177
|
+
|
|
178
|
+
// Fall back to simple numbered input if raw mode is unavailable
|
|
179
|
+
if (!stdin.isTTY || !stdin.setRawMode) {
|
|
180
|
+
return askChoiceFallback(question, options, selected);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return new Promise((resolve) => {
|
|
184
|
+
// Pause readline so it doesn't compete for stdin
|
|
185
|
+
if (rl) { rl.pause(); }
|
|
186
|
+
|
|
187
|
+
console.log(`\n${c.cyan('?')} ${question} ${c.dim('(↑↓ to move, enter to select)')}`);
|
|
188
|
+
|
|
189
|
+
function renderLine(i: number): string {
|
|
190
|
+
const opt = options[i]!;
|
|
191
|
+
const marker = i === selected ? c.cyan('❯') : ' ';
|
|
192
|
+
const label = i === selected ? c.cyan(opt.label) : opt.label;
|
|
193
|
+
const desc = opt.description ? ` ${c.dim(`- ${opt.description}`)}` : '';
|
|
194
|
+
return ` ${marker} ${label}${desc}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function render() {
|
|
198
|
+
// Move cursor up to overwrite previous render
|
|
199
|
+
options.forEach(() => process.stdout.write('\x1b[A'));
|
|
200
|
+
for (let i = 0; i < options.length; i++) {
|
|
201
|
+
process.stdout.write(`\r\x1b[K${renderLine(i)}\n`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Initial draw
|
|
206
|
+
for (let i = 0; i < options.length; i++) {
|
|
207
|
+
console.log(renderLine(i));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const wasRaw = stdin.isRaw;
|
|
211
|
+
stdin.setRawMode(true);
|
|
212
|
+
stdin.resume();
|
|
213
|
+
|
|
214
|
+
const cleanup = () => {
|
|
215
|
+
stdin.removeListener('data', onData);
|
|
216
|
+
try { stdin.setRawMode(wasRaw ?? false); } catch { /* already restored */ }
|
|
217
|
+
if (rl) { rl.resume(); }
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const onData = (data: Buffer) => {
|
|
221
|
+
try {
|
|
222
|
+
const key = data.toString();
|
|
223
|
+
|
|
224
|
+
if (key === '\x1b[A' || key === 'k') {
|
|
225
|
+
selected = (selected - 1 + options.length) % options.length;
|
|
226
|
+
render();
|
|
227
|
+
} else if (key === '\x1b[B' || key === 'j') {
|
|
228
|
+
selected = (selected + 1) % options.length;
|
|
229
|
+
render();
|
|
230
|
+
} else if (key === '\r' || key === '\n') {
|
|
231
|
+
cleanup();
|
|
232
|
+
// Overwrite the list with final selection
|
|
233
|
+
for (let i = 0; i < options.length; i++) process.stdout.write('\x1b[A');
|
|
234
|
+
for (let i = 0; i < options.length; i++) {
|
|
235
|
+
process.stdout.write(`\r\x1b[K`);
|
|
236
|
+
if (i < options.length - 1) process.stdout.write('\n');
|
|
237
|
+
}
|
|
238
|
+
process.stdout.write(`\r\x1b[K ${c.green('✓')} ${options[selected]!.label}\n`);
|
|
239
|
+
resolve(options[selected]!.value);
|
|
240
|
+
} else if (key === '\x03') {
|
|
241
|
+
cleanup();
|
|
242
|
+
process.stdout.write('\n');
|
|
243
|
+
process.exit(130);
|
|
244
|
+
} else {
|
|
245
|
+
const num = parseInt(key, 10);
|
|
246
|
+
if (num >= 1 && num <= options.length) {
|
|
247
|
+
selected = num - 1;
|
|
248
|
+
render();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} catch {
|
|
252
|
+
cleanup();
|
|
253
|
+
resolve(options[selected]!.value);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
stdin.on('data', onData);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Simple fallback for non-TTY environments. */
|
|
262
|
+
function askChoiceFallback<T extends string>(
|
|
263
|
+
question: string,
|
|
264
|
+
options: { label: string; value: T; description?: string }[],
|
|
265
|
+
defaultIdx: number,
|
|
266
|
+
): Promise<T> {
|
|
267
|
+
return new Promise((resolve) => {
|
|
268
|
+
console.log(`\n${c.cyan('?')} ${question}`);
|
|
269
|
+
options.forEach((opt, i) => {
|
|
270
|
+
const marker = i === defaultIdx ? c.cyan('>') : ' ';
|
|
271
|
+
const desc = opt.description ? ` ${c.dim(`- ${opt.description}`)}` : '';
|
|
272
|
+
console.log(` ${marker} ${c.bold(`${i + 1}.`)} ${opt.label}${desc}`);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
getRL().question(`${c.dim(`Enter choice [1-${options.length}]`)}: `, (answer) => {
|
|
276
|
+
const a = answer.trim();
|
|
277
|
+
if (a === '') { resolve(options[defaultIdx]!.value); return; }
|
|
278
|
+
const idx = parseInt(a, 10) - 1;
|
|
279
|
+
if (idx >= 0 && idx < options.length) {
|
|
280
|
+
resolve(options[idx]!.value);
|
|
281
|
+
} else {
|
|
282
|
+
const match = options.find(o =>
|
|
283
|
+
o.value.toLowerCase() === a.toLowerCase() ||
|
|
284
|
+
o.label.toLowerCase() === a.toLowerCase()
|
|
285
|
+
);
|
|
286
|
+
resolve(match ? match.value : options[defaultIdx]!.value);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ── Display Helpers ──────────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Print a step indicator: [2/9] Title
|
|
296
|
+
*/
|
|
297
|
+
export function printStep(current: number, total: number, title: string): void {
|
|
298
|
+
console.log(`\n${c.cyan(`[${current}/${total}]`)} ${c.bold(title)}`);
|
|
299
|
+
console.log(c.dim('─'.repeat(50)));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Print the JARVIS ASCII banner.
|
|
304
|
+
*/
|
|
305
|
+
export function printBanner(): void {
|
|
306
|
+
console.log(c.cyan(`
|
|
307
|
+
██╗ █████╗ ██████╗ ██╗ ██╗██╗███████╗
|
|
308
|
+
██║██╔══██╗██╔══██╗██║ ██║██║██╔════╝
|
|
309
|
+
██║███████║██████╔╝██║ ██║██║███████╗
|
|
310
|
+
██ ██║██╔══██║██╔══██╗╚██╗ ██╔╝██║╚════██║
|
|
311
|
+
╚█████╔╝██║ ██║██║ ██║ ╚████╔╝ ██║███████║
|
|
312
|
+
╚════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═══╝ ╚═╝╚══════╝`));
|
|
313
|
+
console.log(c.dim(' Just A Rather Very Intelligent System\n'));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Print a success message with checkmark.
|
|
318
|
+
*/
|
|
319
|
+
export function printOk(message: string): void {
|
|
320
|
+
console.log(` ${c.green('✓')} ${message}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Print a warning message.
|
|
325
|
+
*/
|
|
326
|
+
export function printWarn(message: string): void {
|
|
327
|
+
console.log(` ${c.yellow('!')} ${message}`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Print an error message.
|
|
332
|
+
*/
|
|
333
|
+
export function printErr(message: string): void {
|
|
334
|
+
console.log(` ${c.red('✗')} ${message}`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Print an info/skip message.
|
|
339
|
+
*/
|
|
340
|
+
export function printInfo(message: string): void {
|
|
341
|
+
console.log(` ${c.dim('○')} ${message}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ── Spinner ──────────────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
347
|
+
|
|
348
|
+
export interface Spinner {
|
|
349
|
+
stop: (message?: string) => void;
|
|
350
|
+
update: (text: string) => void;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Start a CLI spinner. Returns { stop(msg?), update(text) }.
|
|
355
|
+
*/
|
|
356
|
+
export function startSpinner(text: string): Spinner {
|
|
357
|
+
let frame = 0;
|
|
358
|
+
let currentText = text;
|
|
359
|
+
let stopped = false;
|
|
360
|
+
|
|
361
|
+
const interval = setInterval(() => {
|
|
362
|
+
if (stopped) return;
|
|
363
|
+
process.stdout.write(`\r ${c.cyan(SPINNER_FRAMES[frame % SPINNER_FRAMES.length]!)} ${currentText}`);
|
|
364
|
+
frame++;
|
|
365
|
+
}, 80);
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
stop(message?: string) {
|
|
369
|
+
if (stopped) return;
|
|
370
|
+
stopped = true;
|
|
371
|
+
clearInterval(interval);
|
|
372
|
+
process.stdout.write('\r' + ' '.repeat(currentText.length + 10) + '\r');
|
|
373
|
+
if (message) printOk(message);
|
|
374
|
+
},
|
|
375
|
+
update(newText: string) {
|
|
376
|
+
currentText = newText;
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ── Utility ──────────────────────────────────────────────────────────
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Detect platform context.
|
|
385
|
+
*/
|
|
386
|
+
export function detectPlatform(): 'macos' | 'linux' | 'wsl' {
|
|
387
|
+
if (process.platform === 'darwin') return 'macos';
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
if (existsSync('/proc/version')) {
|
|
391
|
+
const text = readFileSync('/proc/version', 'utf-8').toLowerCase();
|
|
392
|
+
if (text.includes('microsoft') || text.includes('wsl')) {
|
|
393
|
+
return 'wsl';
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
} catch {
|
|
397
|
+
// Not Linux or can't read /proc
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return 'linux';
|
|
401
|
+
}
|