@yeaft/webchat-agent 0.0.233 → 0.0.235

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.
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Crew — UI 消息辅助函数
3
+ * sendCrewMessage, sendCrewOutput, sendStatusUpdate, endRoleStreaming, findActiveRole
4
+ */
5
+ import ctx from '../context.js';
6
+ import { upsertCrewIndex, saveSessionMeta } from './persistence.js';
7
+
8
+ /**
9
+ * 发送 crew 消息到 server(透传到 Web)
10
+ */
11
+ export function sendCrewMessage(msg) {
12
+ if (ctx.sendToServer) {
13
+ ctx.sendToServer(msg);
14
+ }
15
+ }
16
+
17
+ /** Format role label: "icon displayName" or just "displayName" if no icon */
18
+ function roleLabel(r) {
19
+ return r.icon ? `${r.icon} ${r.displayName}` : r.displayName;
20
+ }
21
+
22
+ /**
23
+ * 结束指定角色的最后一条 streaming 消息(反向搜索)
24
+ */
25
+ export function endRoleStreaming(session, roleName) {
26
+ for (let i = session.uiMessages.length - 1; i >= 0; i--) {
27
+ if (session.uiMessages[i].role === roleName && session.uiMessages[i]._streaming) {
28
+ delete session.uiMessages[i]._streaming;
29
+ break;
30
+ }
31
+ }
32
+ }
33
+
34
+ /**
35
+ * 找到当前活跃的角色(最近一个 turnActive 的)
36
+ */
37
+ export function findActiveRole(session) {
38
+ for (const [name, state] of session.roleStates) {
39
+ if (state.turnActive) return name;
40
+ }
41
+ return null;
42
+ }
43
+
44
+ /**
45
+ * 发送角色输出到 Web
46
+ */
47
+ export function sendCrewOutput(session, roleName, outputType, rawMessage, extra = {}) {
48
+ const role = session.roles.get(roleName);
49
+ const roleIcon = role?.icon || '';
50
+ const displayName = role?.displayName || roleName;
51
+
52
+ // 从 roleState 获取当前 task 信息
53
+ const roleState = session.roleStates.get(roleName);
54
+ const taskId = roleState?.currentTask?.taskId || null;
55
+ const taskTitle = roleState?.currentTask?.taskTitle || null;
56
+
57
+ sendCrewMessage({
58
+ type: 'crew_output',
59
+ sessionId: session.id,
60
+ role: roleName,
61
+ roleIcon,
62
+ roleName: displayName,
63
+ outputType, // 'text' | 'tool_use' | 'tool_result' | 'route' | 'system'
64
+ data: rawMessage,
65
+ taskId,
66
+ taskTitle,
67
+ ...extra
68
+ });
69
+
70
+ // ★ 累积 feature 到持久化列表
71
+ if (taskId && taskTitle && !session.features.has(taskId)) {
72
+ session.features.set(taskId, { taskId, taskTitle, createdAt: Date.now() });
73
+ }
74
+
75
+ // ★ 记录精简 UI 消息用于恢复(跳过 tool_use/tool_result,只记录可见内容)
76
+ if (outputType === 'text') {
77
+ const content = rawMessage?.message?.content;
78
+ let text = '';
79
+ if (typeof content === 'string') {
80
+ text = content;
81
+ } else if (Array.isArray(content)) {
82
+ text = content.filter(b => b.type === 'text').map(b => b.text).join('');
83
+ }
84
+ if (!text) return;
85
+ // ★ 反向搜索该角色最后一条 _streaming 消息
86
+ let found = false;
87
+ for (let i = session.uiMessages.length - 1; i >= 0; i--) {
88
+ const msg = session.uiMessages[i];
89
+ if (msg.role === roleName && msg.type === 'text' && msg._streaming) {
90
+ msg.content += text;
91
+ found = true;
92
+ break;
93
+ }
94
+ }
95
+ if (!found) {
96
+ session.uiMessages.push({
97
+ role: roleName, roleIcon, roleName: displayName,
98
+ type: 'text', content: text, _streaming: true,
99
+ taskId, taskTitle,
100
+ timestamp: Date.now()
101
+ });
102
+ }
103
+ } else if (outputType === 'route') {
104
+ // 结束该角色前一条 streaming
105
+ endRoleStreaming(session, roleName);
106
+ session.uiMessages.push({
107
+ role: roleName, roleIcon, roleName: displayName,
108
+ type: 'route', routeTo: extra.routeTo,
109
+ routeSummary: extra.routeSummary || '',
110
+ content: `→ @${extra.routeTo} ${extra.routeSummary || ''}`,
111
+ taskId, taskTitle,
112
+ timestamp: Date.now()
113
+ });
114
+ } else if (outputType === 'system') {
115
+ const content = rawMessage?.message?.content;
116
+ let text = '';
117
+ if (typeof content === 'string') {
118
+ text = content;
119
+ } else if (Array.isArray(content)) {
120
+ text = content.filter(b => b.type === 'text').map(b => b.text).join('');
121
+ }
122
+ if (!text) return;
123
+ session.uiMessages.push({
124
+ role: roleName, roleIcon, roleName: displayName,
125
+ type: 'system', content: text,
126
+ timestamp: Date.now()
127
+ });
128
+ } else if (outputType === 'tool_use') {
129
+ // 结束该角色前一条 streaming
130
+ endRoleStreaming(session, roleName);
131
+ const content = rawMessage?.message?.content;
132
+ if (Array.isArray(content)) {
133
+ for (const block of content) {
134
+ if (block.type === 'tool_use') {
135
+ // Save trimmed toolInput for restore
136
+ const input = block.input || {};
137
+ let savedInput;
138
+ if (block.name === 'TodoWrite') {
139
+ savedInput = input;
140
+ } else {
141
+ const trimmedInput = {};
142
+ if (input.file_path) trimmedInput.file_path = input.file_path;
143
+ if (input.command) trimmedInput.command = input.command.substring(0, 200);
144
+ if (input.pattern) trimmedInput.pattern = input.pattern;
145
+ if (input.old_string) trimmedInput.old_string = input.old_string.substring(0, 100);
146
+ if (input.new_string) trimmedInput.new_string = input.new_string.substring(0, 100);
147
+ if (input.url) trimmedInput.url = input.url;
148
+ if (input.query) trimmedInput.query = input.query;
149
+ savedInput = Object.keys(trimmedInput).length > 0 ? trimmedInput : null;
150
+ }
151
+ session.uiMessages.push({
152
+ role: roleName, roleIcon, roleName: displayName,
153
+ type: 'tool',
154
+ toolName: block.name,
155
+ toolId: block.id,
156
+ toolInput: savedInput,
157
+ content: `${block.name} ${block.input?.file_path || block.input?.command?.substring(0, 60) || ''}`,
158
+ hasResult: false,
159
+ taskId, taskTitle,
160
+ timestamp: Date.now()
161
+ });
162
+ }
163
+ }
164
+ }
165
+ } else if (outputType === 'tool_result') {
166
+ // 标记对应 tool 的 hasResult
167
+ const toolId = rawMessage?.message?.tool_use_id;
168
+ if (toolId) {
169
+ for (let i = session.uiMessages.length - 1; i >= 0; i--) {
170
+ if (session.uiMessages[i].type === 'tool' && session.uiMessages[i].toolId === toolId) {
171
+ session.uiMessages[i].hasResult = true;
172
+ break;
173
+ }
174
+ }
175
+ }
176
+ // Check for image blocks in tool_result content
177
+ const resultContent = rawMessage?.message?.content;
178
+ if (Array.isArray(resultContent)) {
179
+ for (const item of resultContent) {
180
+ if (item.type === 'image' && item.source?.type === 'base64') {
181
+ sendCrewMessage({
182
+ type: 'crew_image',
183
+ sessionId: session.id,
184
+ role: roleName,
185
+ roleIcon,
186
+ roleName: displayName,
187
+ toolId: toolId || '',
188
+ mimeType: item.source.media_type,
189
+ data: item.source.data,
190
+ taskId, taskTitle
191
+ });
192
+ session.uiMessages.push({
193
+ role: roleName, roleIcon, roleName: displayName,
194
+ type: 'image', toolId: toolId || '',
195
+ mimeType: item.source.media_type,
196
+ taskId, taskTitle,
197
+ timestamp: Date.now()
198
+ });
199
+ }
200
+ }
201
+ }
202
+ }
203
+ // tool 只保存精简信息(toolName + 摘要),不存完整 toolInput/toolResult
204
+ }
205
+
206
+ /**
207
+ * 发送 session 状态更新
208
+ */
209
+ export function sendStatusUpdate(session) {
210
+ const currentRole = findActiveRole(session);
211
+
212
+ sendCrewMessage({
213
+ type: 'crew_status',
214
+ sessionId: session.id,
215
+ status: session.status,
216
+ currentRole,
217
+ round: session.round,
218
+ costUsd: session.costUsd,
219
+ totalInputTokens: session.totalInputTokens,
220
+ totalOutputTokens: session.totalOutputTokens,
221
+ roles: Array.from(session.roles.values()).map(r => ({
222
+ name: r.name,
223
+ displayName: r.displayName,
224
+ icon: r.icon,
225
+ description: r.description,
226
+ isDecisionMaker: r.isDecisionMaker || false,
227
+ model: r.model,
228
+ roleType: r.roleType,
229
+ groupIndex: r.groupIndex
230
+ })),
231
+ activeRoles: Array.from(session.roleStates.entries())
232
+ .filter(([, s]) => s.turnActive)
233
+ .map(([name]) => name),
234
+ currentToolByRole: Object.fromEntries(
235
+ Array.from(session.roleStates.entries())
236
+ .filter(([, s]) => s.turnActive && s.currentTool)
237
+ .map(([name, s]) => [name, s.currentTool])
238
+ ),
239
+ features: Array.from(session.features.values()),
240
+ initProgress: session.initProgress || null
241
+ });
242
+
243
+ // 异步更新持久化
244
+ upsertCrewIndex(session).catch(e => console.warn('[Crew] Failed to update index:', e.message));
245
+ saveSessionMeta(session).catch(e => console.warn('[Crew] Failed to save session meta:', e.message));
246
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Crew — Git Worktree 管理
3
+ * 为开发组创建/清理 git worktrees
4
+ */
5
+ import { promises as fs } from 'fs';
6
+ import { join } from 'path';
7
+ import { execFile as execFileCb } from 'child_process';
8
+ import { promisify } from 'util';
9
+
10
+ const execFile = promisify(execFileCb);
11
+
12
+ /**
13
+ * 为开发组创建 git worktree
14
+ * 每个 groupIndex 对应一个 worktree,同组的 dev/rev/test 共享
15
+ *
16
+ * @param {string} projectDir - 主项目目录
17
+ * @param {Array} roles - 展开后的角色列表
18
+ * @returns {Map<number, string>} groupIndex → worktree 路径
19
+ */
20
+ export async function initWorktrees(projectDir, roles) {
21
+ const groupIndices = [...new Set(roles.filter(r => r.groupIndex > 0).map(r => r.groupIndex))];
22
+ if (groupIndices.length === 0) return new Map();
23
+
24
+ const worktreeBase = join(projectDir, '.worktrees');
25
+ await fs.mkdir(worktreeBase, { recursive: true });
26
+
27
+ // 获取 git 已知的 worktree 列表
28
+ let knownWorktrees = new Set();
29
+ try {
30
+ const { stdout } = await execFile('git', ['worktree', 'list', '--porcelain'], { cwd: projectDir });
31
+ for (const line of stdout.split('\n')) {
32
+ if (line.startsWith('worktree ')) {
33
+ knownWorktrees.add(line.slice('worktree '.length).trim());
34
+ }
35
+ }
36
+ } catch {
37
+ // git worktree list 失败,视为空集
38
+ }
39
+
40
+ const worktreeMap = new Map();
41
+
42
+ for (const idx of groupIndices) {
43
+ const wtDir = join(worktreeBase, `dev-${idx}`);
44
+ const branch = `crew/dev-${idx}`;
45
+
46
+ // 检查目录是否存在
47
+ let dirExists = false;
48
+ try {
49
+ await fs.access(wtDir);
50
+ dirExists = true;
51
+ } catch {}
52
+
53
+ if (dirExists) {
54
+ if (knownWorktrees.has(wtDir)) {
55
+ // 目录存在且 git 记录中也有,直接复用
56
+ console.log(`[Crew] Worktree already exists: ${wtDir}`);
57
+ worktreeMap.set(idx, wtDir);
58
+ continue;
59
+ } else {
60
+ // 孤立目录:目录存在但 git 不认识,先删除再重建
61
+ console.warn(`[Crew] Orphaned worktree dir, removing: ${wtDir}`);
62
+ await fs.rm(wtDir, { recursive: true, force: true }).catch(() => {});
63
+ }
64
+ }
65
+
66
+ try {
67
+ // 创建分支(如果不存在)
68
+ try {
69
+ await execFile('git', ['branch', branch], { cwd: projectDir });
70
+ } catch {
71
+ // 分支已存在,忽略
72
+ }
73
+
74
+ // 创建 worktree
75
+ await execFile('git', ['worktree', 'add', wtDir, branch], { cwd: projectDir });
76
+ console.log(`[Crew] Created worktree: ${wtDir} on branch ${branch}`);
77
+ worktreeMap.set(idx, wtDir);
78
+ } catch (e) {
79
+ console.error(`[Crew] Failed to create worktree for group ${idx}:`, e.message);
80
+ }
81
+ }
82
+
83
+ return worktreeMap;
84
+ }
85
+
86
+ /**
87
+ * 清理 session 的 git worktrees
88
+ * @param {string} projectDir - 主项目目录
89
+ */
90
+ export async function cleanupWorktrees(projectDir) {
91
+ const worktreeBase = join(projectDir, '.worktrees');
92
+
93
+ try {
94
+ await fs.access(worktreeBase);
95
+ } catch {
96
+ return; // .worktrees 目录不存在,无需清理
97
+ }
98
+
99
+ try {
100
+ const entries = await fs.readdir(worktreeBase);
101
+ for (const entry of entries) {
102
+ if (!entry.startsWith('dev-')) continue;
103
+ const wtDir = join(worktreeBase, entry);
104
+ const branch = `crew/${entry}`;
105
+
106
+ try {
107
+ await execFile('git', ['worktree', 'remove', wtDir, '--force'], { cwd: projectDir });
108
+ console.log(`[Crew] Removed worktree: ${wtDir}`);
109
+ } catch (e) {
110
+ console.warn(`[Crew] Failed to remove worktree ${wtDir}:`, e.message);
111
+ }
112
+
113
+ try {
114
+ await execFile('git', ['branch', '-D', branch], { cwd: projectDir });
115
+ console.log(`[Crew] Deleted branch: ${branch}`);
116
+ } catch (e) {
117
+ console.warn(`[Crew] Failed to delete branch ${branch}:`, e.message);
118
+ }
119
+ }
120
+
121
+ // 尝试删除 .worktrees 目录(如果已空)
122
+ try {
123
+ await fs.rmdir(worktreeBase);
124
+ } catch {
125
+ // 目录不空或其他原因,忽略
126
+ }
127
+ } catch (e) {
128
+ console.error(`[Crew] Failed to cleanup worktrees:`, e.message);
129
+ }
130
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.0.233",
3
+ "version": "0.0.235",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -33,8 +33,12 @@
33
33
  },
34
34
  "files": [
35
35
  "*.js",
36
+ "connection/",
37
+ "crew/",
36
38
  "sdk/",
37
- "scripts/"
39
+ "scripts/",
40
+ "service/",
41
+ "workbench/"
38
42
  ],
39
43
  "license": "MIT",
40
44
  "author": "Yeaft",
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Service — shared configuration and utility functions
3
+ */
4
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
5
+ import { join, dirname } from 'path';
6
+ import { platform, homedir } from 'os';
7
+ import { fileURLToPath } from 'url';
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ export const SERVICE_NAME = 'yeaft-agent';
11
+
12
+ /**
13
+ * Load .env file from agent directory (or cwd) into process.env
14
+ * Only sets vars that are not already set (won't override existing env)
15
+ */
16
+ function loadDotenv() {
17
+ // Try agent source directory first, then cwd
18
+ const agentDir = join(__dirname, '..');
19
+ const candidates = [join(agentDir, '.env'), join(process.cwd(), '.env')];
20
+ for (const envPath of candidates) {
21
+ if (!existsSync(envPath)) continue;
22
+ try {
23
+ const content = readFileSync(envPath, 'utf-8');
24
+ for (const line of content.split('\n')) {
25
+ const match = line.match(/^\s*([^#][^=]*)\s*=\s*(.*)$/);
26
+ if (match) {
27
+ const key = match[1].trim();
28
+ let value = match[2].trim();
29
+ value = value.replace(/^["']|["']$/g, '');
30
+ // Don't override existing env vars
31
+ if (!process.env[key]) {
32
+ process.env[key] = value;
33
+ }
34
+ }
35
+ }
36
+ return; // loaded successfully, stop
37
+ } catch {
38
+ // continue to next candidate
39
+ }
40
+ }
41
+ }
42
+
43
+ // Standard config/log directory per platform
44
+ export function getConfigDir() {
45
+ if (platform() === 'win32') {
46
+ return join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), SERVICE_NAME);
47
+ }
48
+ return join(homedir(), '.config', SERVICE_NAME);
49
+ }
50
+
51
+ export function getLogDir() {
52
+ return join(getConfigDir(), 'logs');
53
+ }
54
+
55
+ export function getConfigPath() {
56
+ return join(getConfigDir(), 'config.json');
57
+ }
58
+
59
+ /** Save agent configuration to standard location */
60
+ export function saveServiceConfig(config) {
61
+ const dir = getConfigDir();
62
+ mkdirSync(dir, { recursive: true });
63
+ mkdirSync(getLogDir(), { recursive: true });
64
+ writeFileSync(getConfigPath(), JSON.stringify(config, null, 2));
65
+ }
66
+
67
+ /** Load agent configuration from standard location */
68
+ export function loadServiceConfig() {
69
+ const configPath = getConfigPath();
70
+ if (!existsSync(configPath)) return null;
71
+ try {
72
+ return JSON.parse(readFileSync(configPath, 'utf-8'));
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ /** Resolve the full path to the node binary */
79
+ export function getNodePath() {
80
+ return process.execPath;
81
+ }
82
+
83
+ /** Resolve the full path to cli.js */
84
+ export function getCliPath() {
85
+ return join(__dirname, '..', 'cli.js');
86
+ }
87
+
88
+ /**
89
+ * Parse --server/--name/--secret/--work-dir from args, merge with existing config
90
+ */
91
+ export function parseServiceArgs(args) {
92
+ // Load .env if available (for dev / source-based usage)
93
+ loadDotenv();
94
+
95
+ const existing = loadServiceConfig() || {};
96
+ const config = {
97
+ serverUrl: existing.serverUrl || '',
98
+ agentName: existing.agentName || '',
99
+ agentSecret: existing.agentSecret || '',
100
+ workDir: existing.workDir || '',
101
+ };
102
+
103
+ // Environment variables override saved config
104
+ if (process.env.SERVER_URL) config.serverUrl = process.env.SERVER_URL;
105
+ if (process.env.AGENT_NAME) config.agentName = process.env.AGENT_NAME;
106
+ if (process.env.AGENT_SECRET) config.agentSecret = process.env.AGENT_SECRET;
107
+ if (process.env.WORK_DIR) config.workDir = process.env.WORK_DIR;
108
+
109
+ // CLI args override everything
110
+ for (let i = 0; i < args.length; i++) {
111
+ const arg = args[i];
112
+ const next = args[i + 1];
113
+ switch (arg) {
114
+ case '--server': if (next) { config.serverUrl = next; i++; } break;
115
+ case '--name': if (next) { config.agentName = next; i++; } break;
116
+ case '--secret': if (next) { config.agentSecret = next; i++; } break;
117
+ case '--work-dir': if (next) { config.workDir = next; i++; } break;
118
+ }
119
+ }
120
+
121
+ return config;
122
+ }
123
+
124
+ export function validateConfig(config) {
125
+ if (!config.serverUrl) {
126
+ console.error('Error: --server <url> is required');
127
+ process.exit(1);
128
+ }
129
+ if (!config.agentSecret) {
130
+ console.error('Error: --secret <secret> is required');
131
+ process.exit(1);
132
+ }
133
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Service — platform dispatcher
3
+ * Routes install/uninstall/start/stop/restart/status/logs to the correct platform module.
4
+ */
5
+ import { existsSync } from 'fs';
6
+ import { platform } from 'os';
7
+ import {
8
+ SERVICE_NAME, getConfigDir, getLogDir, getConfigPath,
9
+ saveServiceConfig, loadServiceConfig,
10
+ parseServiceArgs, validateConfig
11
+ } from './config.js';
12
+ import { getSystemdServicePath, linuxInstall, linuxUninstall, linuxStart, linuxStop, linuxRestart, linuxStatus, linuxLogs } from './linux.js';
13
+ import { getLaunchdPlistPath, macInstall, macUninstall, macStart, macStop, macRestart, macStatus, macLogs } from './macos.js';
14
+ import { winInstall, winUninstall, winStart, winStop, winRestart, winStatus, winLogs } from './windows.js';
15
+
16
+ export {
17
+ getConfigDir, getLogDir, getConfigPath,
18
+ saveServiceConfig, loadServiceConfig,
19
+ parseServiceArgs
20
+ };
21
+
22
+ const os = platform();
23
+
24
+ function ensureInstalled() {
25
+ if (os === 'linux') {
26
+ if (!existsSync(getSystemdServicePath())) {
27
+ console.error('Service not installed. Run "yeaft-agent install" first.');
28
+ process.exit(1);
29
+ }
30
+ } else if (os === 'darwin') {
31
+ if (!existsSync(getLaunchdPlistPath())) {
32
+ console.error('Service not installed. Run "yeaft-agent install" first.');
33
+ process.exit(1);
34
+ }
35
+ }
36
+ // Windows check is done inside individual functions
37
+ }
38
+
39
+ export function install(args) {
40
+ const config = parseServiceArgs(args);
41
+ validateConfig(config);
42
+ saveServiceConfig(config);
43
+
44
+ console.log(`Installing ${SERVICE_NAME} service...`);
45
+ console.log(` Server: ${config.serverUrl}`);
46
+ console.log(` Name: ${config.agentName || '(auto)'}`);
47
+ console.log(` WorkDir: ${config.workDir || '(home)'}`);
48
+ console.log('');
49
+
50
+ if (os === 'linux') linuxInstall(config);
51
+ else if (os === 'darwin') macInstall(config);
52
+ else if (os === 'win32') winInstall(config);
53
+ else {
54
+ console.error(`Unsupported platform: ${os}`);
55
+ console.log('You can run the agent directly: yeaft-agent --server <url> --secret <secret>');
56
+ process.exit(1);
57
+ }
58
+ }
59
+
60
+ export function uninstall() {
61
+ console.log(`Uninstalling ${SERVICE_NAME} service...`);
62
+ if (os === 'linux') linuxUninstall();
63
+ else if (os === 'darwin') macUninstall();
64
+ else if (os === 'win32') winUninstall();
65
+ else { console.error(`Unsupported platform: ${os}`); process.exit(1); }
66
+ }
67
+
68
+ export function start() {
69
+ ensureInstalled();
70
+ if (os === 'linux') linuxStart();
71
+ else if (os === 'darwin') macStart();
72
+ else if (os === 'win32') winStart();
73
+ }
74
+
75
+ export function stop() {
76
+ ensureInstalled();
77
+ if (os === 'linux') linuxStop();
78
+ else if (os === 'darwin') macStop();
79
+ else if (os === 'win32') winStop();
80
+ }
81
+
82
+ export function restart() {
83
+ ensureInstalled();
84
+ if (os === 'linux') linuxRestart();
85
+ else if (os === 'darwin') macRestart();
86
+ else if (os === 'win32') winRestart();
87
+ }
88
+
89
+ export function status() {
90
+ if (os === 'linux') linuxStatus();
91
+ else if (os === 'darwin') macStatus();
92
+ else if (os === 'win32') winStatus();
93
+ }
94
+
95
+ export function logs() {
96
+ if (os === 'linux') linuxLogs();
97
+ else if (os === 'darwin') macLogs();
98
+ else if (os === 'win32') winLogs();
99
+ }