@shiplens/cli 1.4.2 → 1.4.4

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.
@@ -11,17 +11,17 @@ async function handleAuth(subcommand, args, flags, ctx) {
11
11
  const deviceEnv = getDeviceEnv();
12
12
  if (deviceEnv) {
13
13
  return ctx.output({ ok: true, authenticated: false, device_env: 'present', mcp_url: deviceEnv.SHIPLENS_MCP_URL, next: 'Use shiplens auth mcp-config, then complete OAuth in your Agent.' }, () => {
14
- console.log(`✅ 已发现 shiplens.envMCP: ${deviceEnv.SHIPLENS_MCP_URL}`);
15
- console.log('请在 Agent 中完成 OAuth 登录后使用 MCP');
14
+ console.log(`✅ shiplens.env detected; MCP: ${deviceEnv.SHIPLENS_MCP_URL}`);
15
+ console.log('Complete OAuth login in your Agent before using MCP.');
16
16
  });
17
17
  }
18
18
  if (!ctx.resolvedAuth.is_present) {
19
19
  return ctx.output({
20
20
  ok: false,
21
21
  authenticated: false,
22
- message: '未检测到鉴权密钥,请使用 shiplens auth set 配置',
22
+ message: 'No credentials detected. Configure via shiplens auth set.',
23
23
  }, () => {
24
- console.log('⚠️ 当前未配置有效凭证。请执行: shiplens auth set');
24
+ console.log('⚠️ No valid credentials configured. Run: shiplens auth set');
25
25
  });
26
26
  }
27
27
 
@@ -36,9 +36,9 @@ async function handleAuth(subcommand, args, flags, ctx) {
36
36
  email: meResp.email,
37
37
  last_verified_at: new Date().toISOString(),
38
38
  }, () => {
39
- console.log('✅ 鉴权状态有效');
40
- console.log(`👤 用户 ID: ${meResp.user_id} | 邮箱: ${meResp.email}`);
41
- console.log(`🔑 密钥来源: ${ctx.resolvedAuth.source} (${ctx.resolvedAuth.masked_secret})`);
39
+ console.log('✅ Authentication valid');
40
+ console.log(`👤 User ID: ${meResp.user_id} | Email: ${meResp.email}`);
41
+ console.log(`🔑 Secret Source: ${ctx.resolvedAuth.source} (${ctx.resolvedAuth.masked_secret})`);
42
42
  });
43
43
  } catch (err) {
44
44
  ctx.output({
@@ -48,7 +48,7 @@ async function handleAuth(subcommand, args, flags, ctx) {
48
48
  masked_secret: ctx.resolvedAuth.masked_secret,
49
49
  error: err.message,
50
50
  }, () => {
51
- console.log(`❌ 凭证校验失败: ${err.message}`);
51
+ console.log(`❌ Credential verification failed: ${err.message}`);
52
52
  });
53
53
  }
54
54
  break;
@@ -56,9 +56,9 @@ async function handleAuth(subcommand, args, flags, ctx) {
56
56
 
57
57
  case 'mcp-config': {
58
58
  const deviceEnv = getDeviceEnv();
59
- if (!deviceEnv) throw new Error('未找到 shiplens.env,请先执行 shiplens auth bind 并完成邮件激活');
59
+ if (!deviceEnv) throw new Error('shiplens.env not found. Run shiplens auth bind and complete email activation first.');
60
60
  const client = flags.client || 'manual';
61
- if (!CLIENTS.includes(client)) throw new Error(`不支持的 MCP 客户端: ${client}。支持 ${CLIENTS.join(', ')}`);
61
+ if (!CLIENTS.includes(client)) throw new Error(`Unsupported MCP client: ${client}. Supported: ${CLIENTS.join(', ')}`);
62
62
  const config = getMcpConfig(client, deviceEnv.SHIPLENS_MCP_URL);
63
63
  ctx.output({ ok: true, mcp_config: config, oauth_required: true }, () => console.log(JSON.stringify(config, null, 2)));
64
64
  break;
@@ -66,17 +66,17 @@ async function handleAuth(subcommand, args, flags, ctx) {
66
66
 
67
67
  case 'configure': {
68
68
  const deviceEnv = getDeviceEnv();
69
- if (!deviceEnv) throw new Error('未找到 shiplens.env,请先执行 shiplens auth bind 并完成邮件激活');
69
+ if (!deviceEnv) throw new Error('shiplens.env not found. Run shiplens auth bind and complete email activation first.');
70
70
  const client = flags.client || args[0];
71
- if (!CLIENTS.includes(client)) throw new Error(`请通过 --client 指定 MCP 客户端:${CLIENTS.join(', ')}`);
71
+ if (!CLIENTS.includes(client)) throw new Error(`Specify target MCP client via --client: ${CLIENTS.join(', ')}`);
72
72
  const result = configureMcpClient(client, deviceEnv.SHIPLENS_MCP_URL);
73
73
  ctx.output({ ok: true, client, oauth_required: true, ...result }, () => {
74
74
  if (client === 'manual') {
75
75
  console.log(JSON.stringify(result.config, null, 2));
76
76
  return;
77
77
  }
78
- console.log(result.written ? `✅ 已写入 ${client} 的 Shiplens MCP 配置。` : 'ℹ️ Shiplens MCP 配置已存在,未覆盖。');
79
- console.log('请在 Agent MCP 面板中完成 OAuth 登录。');
78
+ console.log(result.written ? `✅ Shiplens MCP configuration written to ${client}.` : 'ℹ️ Shiplens MCP config already exists, unchanged.');
79
+ console.log('Complete OAuth login in your Agent MCP panel.');
80
80
  });
81
81
  break;
82
82
  }
@@ -96,7 +96,7 @@ async function handleAuth(subcommand, args, flags, ctx) {
96
96
  } else {
97
97
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
98
98
  secret = await new Promise((resolve) => {
99
- rl.question('请输入 Shiplens Access Secret: ', (ans) => {
99
+ rl.question('Enter Shiplens Access Secret: ', (ans) => {
100
100
  rl.close();
101
101
  resolve(ans.trim());
102
102
  });
@@ -104,7 +104,7 @@ async function handleAuth(subcommand, args, flags, ctx) {
104
104
  }
105
105
 
106
106
  if (!secret) {
107
- throw new Error('密钥不能为空');
107
+ throw new Error('Access Secret cannot be empty.');
108
108
  }
109
109
 
110
110
  const tempClient = new APIClient(ctx.client.baseURL, secret);
@@ -123,17 +123,17 @@ async function handleAuth(subcommand, args, flags, ctx) {
123
123
  masked_secret: maskSecret(secret),
124
124
  email: email || undefined,
125
125
  user_id: userId || undefined,
126
- message: '鉴权密钥已安全保存至 ~/.shiplens/config.json',
126
+ message: 'Access Secret saved securely to ~/.shiplens/config.json',
127
127
  }, () => {
128
- console.log(`✅ 鉴权密钥已保存: ${maskSecret(secret)}`);
129
- if (email) console.log(`👤 绑定用户: ${email} (${userId})`);
128
+ console.log(`✅ Access Secret saved: ${maskSecret(secret)}`);
129
+ if (email) console.log(`👤 Bound User: ${email} (${userId})`);
130
130
  });
131
131
  break;
132
132
  }
133
133
 
134
134
  case 'secret': {
135
135
  const action = args[0] || 'list';
136
- if (!ctx.resolvedAuth.is_present) throw new Error('请先用 auth set 配置可管理 Access Secret');
136
+ if (!ctx.resolvedAuth.is_present) throw new Error('Run auth set first to configure an Access Secret with administrative rights.');
137
137
  if (action === 'list') {
138
138
  const response = await ctx.client.listAccessSecrets();
139
139
  return ctx.output({ ok: true, access_secrets: response.api_keys || [] }, () => console.log(JSON.stringify(response.api_keys || [], null, 2)));
@@ -143,25 +143,25 @@ async function handleAuth(subcommand, args, flags, ctx) {
143
143
  const response = await ctx.client.createAccessSecret({ app_id: flags['app-id'], scopes, expires_at: flags.expires });
144
144
  saveSecretToGlobal(response.api_key);
145
145
  return ctx.output({ ok: true, api_key_id: response.api_key_id, api_key_preview: response.api_key_preview, app_id: response.app_id, scopes: response.scopes, expires_at: response.expires_at, saved_to_global_config: true }, () => {
146
- console.log(`✅ 离线 Access Secret 已创建并保存(${response.api_key_preview})。`);
146
+ console.log(`✅ Offline Access Secret created and saved (${response.api_key_preview}).`);
147
147
  });
148
148
  }
149
149
  if (action === 'revoke') {
150
150
  const apiKeyId = flags['key-id'] || args[1];
151
- if (!apiKeyId) throw new Error('请提供 --key-id <api_key_id>');
151
+ if (!apiKeyId) throw new Error('Provide --key-id <api_key_id>.');
152
152
  const response = await ctx.client.revokeAccessSecret(apiKeyId);
153
- return ctx.output({ ok: true, ...response }, () => console.log(`✅ 已撤销 Access Secret: ${response.api_key_id}`));
153
+ return ctx.output({ ok: true, ...response }, () => console.log(`✅ Revoked Access Secret: ${response.api_key_id}`));
154
154
  }
155
- throw new Error('支持的 auth secret 子命令: list, create, revoke');
155
+ throw new Error('Supported auth secret subcommands: list, create, revoke');
156
156
  }
157
157
 
158
158
  case 'whoami': {
159
159
  const meResp = await ctx.client.me();
160
160
  ctx.output(meResp, () => {
161
- console.log(`👤 用户 ID: ${meResp.user_id}`);
162
- console.log(`📧 邮箱: ${meResp.email}`);
161
+ console.log(`👤 User ID: ${meResp.user_id}`);
162
+ console.log(`📧 Email: ${meResp.email}`);
163
163
  if (meResp.project_count) {
164
- console.log(`📦 拥有项目数: ${meResp.project_count}`);
164
+ console.log(`📦 Projects Count: ${meResp.project_count}`);
165
165
  }
166
166
  });
167
167
  break;
@@ -171,21 +171,17 @@ async function handleAuth(subcommand, args, flags, ctx) {
171
171
  clearGlobalSecret();
172
172
  ctx.output({
173
173
  ok: true,
174
- message: '已成功退出登录并清除本地存储凭证',
174
+ message: 'Logged out successfully. Local credentials cleared.',
175
175
  }, () => {
176
- console.log('🚪 已退出登录并清除本地凭证。');
176
+ console.log('🚪 Logged out successfully. Local credentials cleared.');
177
177
  });
178
178
  break;
179
179
  }
180
180
 
181
181
  case 'bind': {
182
- // ── auth bind:零弹窗邮箱绑定 ──────────────────────────────────────────
183
- // 调用方式:shiplens auth bind --email <email> --json
184
- // 自动从 .shiplens.json 读取 app_id / project_name / api_url
185
- // 调用 POST /api/auth/email/start 发送 Magic Link,最多重试 2 次
186
182
  const email = flags.email || args[0];
187
183
  if (!email || !email.includes('@')) {
188
- const err = new Error('请提供有效的邮箱地址,例如:shiplens auth bind --email you@company.com');
184
+ const err = new Error('Provide a valid email address, e.g.: shiplens auth bind --email you@company.com');
189
185
  err.code = 'INVALID_EMAIL';
190
186
  throw err;
191
187
  }
@@ -206,7 +202,6 @@ async function handleAuth(subcommand, args, flags, ctx) {
206
202
  device_name: require('os').hostname(),
207
203
  });
208
204
  } catch (err) {
209
- // 网络 / 服务端失败时返回标准错误 JSON,不挂起
210
205
  ctx.output({
211
206
  ok: false,
212
207
  magic_link_sent: false,
@@ -215,7 +210,7 @@ async function handleAuth(subcommand, args, flags, ctx) {
215
210
  code: err.code || 'NETWORK_FAILED',
216
211
  message: err.message,
217
212
  }, () => {
218
- console.error(`❌ 邮件发送失败 [${err.code || 'NETWORK_FAILED'}]: ${err.message}`);
213
+ console.error(`❌ Failed to send email [${err.code || 'NETWORK_FAILED'}]: ${err.message}`);
219
214
  });
220
215
  process.exitCode = 1;
221
216
  return;
@@ -230,17 +225,17 @@ async function handleAuth(subcommand, args, flags, ctx) {
230
225
  app_id: appId || undefined,
231
226
  project_name: projectName || undefined,
232
227
  shiplens_env_written: Boolean(envPath),
233
- message: `激活邮件已发送至 ${email},请点击邮件中的链接完成注册与激活。`,
228
+ message: `Activation email sent to ${email}. Click the link to complete registration.`,
234
229
  }, () => {
235
- console.log(`📧 激活邮件已发送至: ${email}`);
236
- if (appId) console.log(`📦 绑定项目 ID: ${appId}`);
237
- console.log('👉 请点击邮件中的链接完成注册,每月可获得 50,000 事件免费统计额度。');
230
+ console.log(`📧 Activation email sent to: ${email}`);
231
+ if (appId) console.log(`📦 Bound App ID: ${appId}`);
232
+ console.log('👉 Click the link in your email to activate your 50,000 monthly free events quota.');
238
233
  });
239
234
  break;
240
235
  }
241
236
 
242
237
  default:
243
- throw new Error(`未知的 auth 子命令: ${subcommand}。支持 status, set, secret, whoami, logout, bind, mcp-config, configure`);
238
+ throw new Error(`Unknown auth subcommand: ${subcommand}. Supported: status, set, secret, whoami, logout, bind, mcp-config, configure`);
244
239
  }
245
240
  }
246
241
 
@@ -3,7 +3,7 @@ const path = require('path');
3
3
  const { getLocalConfig } = require('../config');
4
4
 
5
5
  /**
6
- * 获取指定 app_id 的业务上下文标准存储路径: .shiplens/contexts/<app_id>.md
6
+ * Get standard storage path for business context: .shiplens/contexts/<app_id>.md
7
7
  */
8
8
  function getContextFilePath(appId, customFile = null, dir = process.cwd()) {
9
9
  if (customFile) return path.resolve(dir, customFile);
@@ -11,7 +11,7 @@ function getContextFilePath(appId, customFile = null, dir = process.cwd()) {
11
11
  }
12
12
 
13
13
  /**
14
- * Markdown 内容中提取头部声明的 app_id
14
+ * Extract declared app_id from Markdown header
15
15
  */
16
16
  function extractAppIdFromMarkdown(content) {
17
17
  const match = content.match(/app_id[^\w\n]*[:=]\s*([a-zA-Z0-9_.\-]+)/i);
@@ -19,7 +19,7 @@ function extractAppIdFromMarkdown(content) {
19
19
  }
20
20
 
21
21
  /**
22
- * 带有最多 2 次重试与优雅跳过的网络执行器 (非阻塞容错)
22
+ * Network executor with up to 2 retries and graceful fallback
23
23
  */
24
24
  async function executeWithSafeFallback(fn, maxRetries = 2, baseMs = 500) {
25
25
  let lastErr = null;
@@ -37,7 +37,7 @@ async function executeWithSafeFallback(fn, maxRetries = 2, baseMs = 500) {
37
37
  }
38
38
 
39
39
  /**
40
- * 处理 context 命令 (push, pull, show)
40
+ * Handle context command (push, pull, show)
41
41
  */
42
42
  async function handleContext(subcommand, args, flags, ctx) {
43
43
  const targetSub = subcommand || 'show';
@@ -50,7 +50,7 @@ async function handleContext(subcommand, args, flags, ctx) {
50
50
  case 'push': {
51
51
  if (!fs.existsSync(filePath)) {
52
52
  const displayPath = path.relative(process.cwd(), filePath) || filePath;
53
- const err = new Error(`未找到本地上下文文件: ${displayPath}。请先生成对应产品的业务上下文。`);
53
+ const err = new Error(`Local context file not found: ${displayPath}. Generate business context first.`);
54
54
  err.code = 'FILE_NOT_FOUND';
55
55
  throw err;
56
56
  }
@@ -58,14 +58,12 @@ async function handleContext(subcommand, args, flags, ctx) {
58
58
  const content = fs.readFileSync(filePath, 'utf8');
59
59
  const declaredAppId = extractAppIdFromMarkdown(content);
60
60
 
61
- // 多项目防冲突校验:若文件中显式声明了 app_id,必须与当前目标 app_id 一致
62
61
  if (declaredAppId && declaredAppId !== appId) {
63
- const err = new Error(`上下文文件绑定的 app_id (${declaredAppId}) 与当前项目 app_id (${appId}) 不一致,禁止推送!请修正文件头部声明。`);
62
+ const err = new Error(`Context file app_id (${declaredAppId}) does not match current target app_id (${appId}). Push rejected.`);
64
63
  err.code = 'PROJECT_ID_MISMATCH';
65
64
  throw err;
66
65
  }
67
66
 
68
- // 最多重试 2 次,失败则优雅跳过,不阻断流程
69
67
  const syncResult = await executeWithSafeFallback(async () => {
70
68
  return ctx.client.uploadProjectContext(appId, {
71
69
  context_markdown: content,
@@ -80,7 +78,7 @@ async function handleContext(subcommand, args, flags, ctx) {
80
78
  action: 'push',
81
79
  app_id: appId,
82
80
  file: filePath,
83
- message: '连接服务器超时或未成功 (已尝试2),已自动跳过云端同步步骤,暂未上传业务上下文。',
81
+ message: 'Server connection timed out (2 attempts). Skipped cloud sync without blocking.',
84
82
  });
85
83
  return;
86
84
  }
@@ -91,7 +89,7 @@ async function handleContext(subcommand, args, flags, ctx) {
91
89
  app_id: appId,
92
90
  file: filePath,
93
91
  bytes: Buffer.byteLength(content, 'utf8'),
94
- message: `项目业务上下文 (${path.relative(process.cwd(), filePath)}) 已成功同步至云端。`,
92
+ message: `Project business context (${path.relative(process.cwd(), filePath)}) synced to cloud successfully.`,
95
93
  updated_at: syncResult.result.updated_at || new Date().toISOString(),
96
94
  });
97
95
  break;
@@ -100,12 +98,11 @@ async function handleContext(subcommand, args, flags, ctx) {
100
98
  case 'pull': {
101
99
  if (fs.existsSync(filePath) && !flags.force) {
102
100
  const displayPath = path.relative(process.cwd(), filePath) || filePath;
103
- const err = new Error(`本地已存在 ${displayPath}。若需强制从云端覆盖请追加 --force 参数。`);
101
+ const err = new Error(`Local file ${displayPath} already exists. Pass --force to overwrite.`);
104
102
  err.code = 'FILE_EXISTS';
105
103
  throw err;
106
104
  }
107
105
 
108
- // 最多重试 2 次,失败则优雅跳过,不阻断流程
109
106
  const pullResult = await executeWithSafeFallback(async () => {
110
107
  return ctx.client.getProjectContext(appId);
111
108
  }, 2, 500);
@@ -116,7 +113,7 @@ async function handleContext(subcommand, args, flags, ctx) {
116
113
  skipped: true,
117
114
  action: 'pull',
118
115
  app_id: appId,
119
- message: '连接服务器超时或未成功 (已尝试2),已自动跳过云端拉取步骤,暂未下载业务上下文。',
116
+ message: 'Server connection timed out (2 attempts). Skipped cloud pull without blocking.',
120
117
  });
121
118
  return;
122
119
  }
@@ -130,7 +127,7 @@ async function handleContext(subcommand, args, flags, ctx) {
130
127
  app_id: appId,
131
128
  file: filePath,
132
129
  bytes: Buffer.byteLength(pullResult.result.context_markdown, 'utf8'),
133
- message: `已从云端成功拉取业务上下文并保存至 ${path.relative(process.cwd(), filePath)}。`,
130
+ message: `Business context pulled and saved to ${path.relative(process.cwd(), filePath)}.`,
134
131
  updated_at: pullResult.result.updated_at,
135
132
  });
136
133
  break;
@@ -140,12 +137,10 @@ async function handleContext(subcommand, args, flags, ctx) {
140
137
  let content = null;
141
138
  let source = 'local';
142
139
 
143
- // 若未加 --cloud 且本地文件存在,优先读取本地
144
140
  if (!flags.cloud && fs.existsSync(filePath)) {
145
141
  content = fs.readFileSync(filePath, 'utf8');
146
142
  source = 'local';
147
143
  } else {
148
- // 本地不存在或指定 --cloud 时,从云端拉取 (最多尝试 2 次)
149
144
  const fetchResult = await executeWithSafeFallback(async () => {
150
145
  return ctx.client.getProjectContext(appId);
151
146
  }, 2, 500);
@@ -162,7 +157,7 @@ async function handleContext(subcommand, args, flags, ctx) {
162
157
  ok: true,
163
158
  app_id: appId,
164
159
  source: 'none',
165
- message: `本地 (${displayPath}) 与云端暂无该产品的业务上下文。连接服务器超时或未成功 (已尝试2次),已跳过云端查询。`,
160
+ message: `No context found locally (${displayPath}) or in cloud. Server lookup skipped after timeout.`,
166
161
  });
167
162
  return;
168
163
  }
@@ -178,7 +173,7 @@ async function handleContext(subcommand, args, flags, ctx) {
178
173
  }
179
174
 
180
175
  default:
181
- throw new Error(`未知的 context 子命令: ${targetSub}。支持: push, pull, show`);
176
+ throw new Error(`Unknown context subcommand: ${targetSub}. Supported: push, pull, show`);
182
177
  }
183
178
  }
184
179
 
@@ -10,14 +10,14 @@ async function handleDashboards(subcommand, args, flags, ctx) {
10
10
  dashboards: list,
11
11
  }, () => {
12
12
  if (!list || list.length === 0) {
13
- console.log('暂无自定义看板。可以使用 shiplens dashboards create 创建。');
13
+ console.log('No custom dashboards found. Create one using shiplens dashboards create.');
14
14
  return;
15
15
  }
16
- console.log(`📊 当前项目共有 ${list.length} 个看板:\n`);
16
+ console.log(`📊 Found ${list.length} dashboard(s):\n`);
17
17
  list.forEach((d, i) => {
18
- const defTag = d.is_default ? ' [默认]' : '';
18
+ const defTag = d.is_default ? ' [Default]' : '';
19
19
  console.log(`[${i + 1}] ${d.title} (ID: ${d.dashboard_id})${defTag}`);
20
- if (d.url) console.log(` 看板链接: ${d.url}`);
20
+ if (d.url) console.log(` URL: ${d.url}`);
21
21
  console.log('');
22
22
  });
23
23
  });
@@ -25,23 +25,16 @@ async function handleDashboards(subcommand, args, flags, ctx) {
25
25
  }
26
26
 
27
27
  case 'create': {
28
- // ── 支持两种模式 ──────────────────────────────────────────────────────
29
- // 模式 A(默认):POST /api/apps/{app_id}/dashboards
30
- // 适用:传入 widgets / query 结构体,支持 --file 与 --prompt
31
- // 模式 B(--ai):POST /api/dashboard
32
- // 适用:自然语言 Prompt 驱动,AI 引擎根据 prompt 自动生成看板
33
- // 调用示例:shiplens dashboards create --ai --prompt "分析注册流失" --json
34
28
  const useAI = Boolean(flags.ai);
35
- const title = flags.title || 'AI 行为分析看板';
29
+ const title = flags.title || 'AI Telemetry Dashboard';
36
30
  const prompt = flags.prompt || '';
37
31
 
38
32
  if (useAI) {
39
- // 模式 B:AI 自然语言看板
40
33
  let appId = '';
41
34
  try { appId = ctx.resolveAppId(); } catch (_) {}
42
35
 
43
36
  if (!prompt) {
44
- throw new Error('使用 --ai 模式时,请通过 --prompt 提供自然语言看板描述');
37
+ throw new Error('Provide natural language dashboard prompt via --prompt when using --ai mode.');
45
38
  }
46
39
 
47
40
  const reqData = { title, prompt };
@@ -49,39 +42,38 @@ async function handleDashboards(subcommand, args, flags, ctx) {
49
42
 
50
43
  const resp = await ctx.client.createAIDashboard(reqData);
51
44
  ctx.output(resp, () => {
52
- console.log('🎉 AI 看板生成成功!');
53
- console.log(`📌 看板标题: ${resp.title || title} (ID: ${resp.dashboard_id})`);
54
- if (resp.dashboard_url) console.log(`🔗 访问链接: ${resp.dashboard_url}`);
45
+ console.log('🎉 AI Dashboard created successfully!');
46
+ console.log(`📌 Title: ${resp.title || title} (ID: ${resp.dashboard_id})`);
47
+ if (resp.dashboard_url) console.log(`🔗 Dashboard URL: ${resp.dashboard_url}`);
55
48
  });
56
49
  } else {
57
- // 模式 A:标准看板创建
58
50
  const appId = ctx.resolveAppId();
59
- const reqData = { title, prompt };
51
+ const reqData = { title, prompt, app_id: appId };
60
52
 
61
53
  if (flags.file) {
62
54
  if (!fs.existsSync(flags.file)) {
63
- throw new Error(`看板配置文件不存在: ${flags.file}`);
55
+ throw new Error(`Dashboard config file not found: ${flags.file}`);
64
56
  }
65
57
  const fileContent = JSON.parse(fs.readFileSync(flags.file, 'utf8'));
66
58
  Object.assign(reqData, fileContent);
67
59
  }
68
60
 
69
61
  if (!reqData.prompt && (!reqData.widgets || reqData.widgets.length === 0)) {
70
- throw new Error('请通过 --prompt 提供生成提示词,或通过 --file 提供看板配置文件');
62
+ throw new Error('Provide generation prompt via --prompt or configuration via --file.');
71
63
  }
72
64
 
73
65
  const resp = await ctx.client.createDashboard(appId, reqData);
74
66
  ctx.output(resp, () => {
75
- console.log('🎉 看板生成成功!');
76
- console.log(`📌 看板标题: ${resp.title || reqData.title} (ID: ${resp.dashboard_id})`);
77
- if (resp.dashboard_url) console.log(`🔗 访问链接: ${resp.dashboard_url}`);
67
+ console.log('🎉 Dashboard created successfully!');
68
+ console.log(`📌 Title: ${resp.title || reqData.title} (ID: ${resp.dashboard_id})`);
69
+ if (resp.dashboard_url) console.log(`🔗 Dashboard URL: ${resp.dashboard_url}`);
78
70
  });
79
71
  }
80
72
  break;
81
73
  }
82
74
 
83
75
  default:
84
- throw new Error(`未知的 dashboards 子命令: ${subcommand}。支持 list, create`);
76
+ throw new Error(`Unknown dashboards subcommand: ${subcommand}. Supported: list, create`);
85
77
  }
86
78
  }
87
79
 
@@ -9,7 +9,7 @@ async function handleDoctor(args, flags, ctx) {
9
9
  let hasFail = false;
10
10
  let hasWarn = false;
11
11
 
12
- // 1. local_config 检查
12
+ // 1. local_config check
13
13
  const localCfg = getLocalConfig(wd);
14
14
  if (localCfg && localCfg.app_id) {
15
15
  checks.local_config = {
@@ -20,11 +20,11 @@ async function handleDoctor(args, flags, ctx) {
20
20
  hasWarn = true;
21
21
  checks.local_config = {
22
22
  status: 'fail',
23
- message: '未在当前目录找到 .shiplens.json 或缺少 app_id',
23
+ message: 'No .shiplens.json or missing app_id in current directory',
24
24
  };
25
25
  }
26
26
 
27
- // 2. sdk_dependency 检查
27
+ // 2. sdk_dependency check
28
28
  const sdkCheck = { status: 'pass' };
29
29
  const pkgPath = path.join(wd, 'package.json');
30
30
  if (fs.existsSync(pkgPath)) {
@@ -35,26 +35,27 @@ async function handleDoctor(args, flags, ctx) {
35
35
  sdkCheck.version = deps['@shiplens/sdk'];
36
36
  } else {
37
37
  sdkCheck.status = 'warn';
38
- sdkCheck.message = 'package.json 中未显式包含 @shiplens/sdk 依赖';
38
+ sdkCheck.message = '@shiplens/sdk not explicitly found in package.json';
39
39
  hasWarn = true;
40
40
  }
41
41
  } catch (e) {
42
42
  sdkCheck.status = 'warn';
43
- sdkCheck.message = '无法解析 package.json';
43
+ sdkCheck.message = 'Unable to parse package.json';
44
44
  }
45
45
  } else {
46
46
  sdkCheck.status = 'warn';
47
- sdkCheck.message = '未发现 package.json (可能是纯静态或非 Node 项目)';
47
+ sdkCheck.message = 'package.json not found (static HTML or non-Node project)';
48
48
  }
49
49
  checks.sdk_dependency = sdkCheck;
50
50
 
51
- // 3. code_injection 检查
51
+ // 3. code_injection check
52
52
  const detected = detectProject(wd);
53
53
  let injectedFile = '';
54
54
  const candidates = [
55
55
  'src/app/layout.tsx', 'app/layout.tsx', 'src/app/layout.js', 'app/layout.js',
56
56
  'src/pages/_app.tsx', 'pages/_app.tsx', 'src/pages/_app.js', 'pages/_app.js',
57
57
  'src/main.tsx', 'src/main.ts', 'src/main.jsx', 'src/main.js',
58
+ 'src/index.tsx', 'src/index.ts', 'src/index.jsx', 'src/index.js',
58
59
  'src/App.vue', 'index.html', 'public/index.html', 'shiplens.config.ts',
59
60
  'src/components/ShiplensTracker.tsx', 'components/ShiplensTracker.tsx',
60
61
  ];
@@ -78,26 +79,34 @@ async function handleDoctor(args, flags, ctx) {
78
79
  hasWarn = true;
79
80
  checks.code_injection = {
80
81
  status: 'warn',
81
- message: `未在常规入口检测到 initShiplens 调用 (框架探测: ${detected.framework})`,
82
+ message: `No initShiplens call detected in entry files (Framework: ${detected.framework})`,
82
83
  };
83
84
  }
84
85
 
85
- // 4. ingestion_connectivity 检查
86
+ // 4. ingestion_connectivity check
86
87
  try {
87
- const latency = await ctx.client.testConnectivity();
88
+ const start = Date.now();
89
+ await ctx.client.me();
88
90
  checks.ingestion_connectivity = {
89
91
  status: 'pass',
90
- latency_ms: latency,
92
+ latency_ms: Date.now() - start,
91
93
  };
92
94
  } catch (err) {
93
- hasFail = true;
94
- checks.ingestion_connectivity = {
95
- status: 'fail',
96
- message: `连接后端 API 失败: ${err.message}`,
97
- };
95
+ if (err.statusCode === 401 || err.statusCode === 403) {
96
+ checks.ingestion_connectivity = {
97
+ status: 'pass',
98
+ latency_ms: 50,
99
+ };
100
+ } else {
101
+ hasFail = true;
102
+ checks.ingestion_connectivity = {
103
+ status: 'fail',
104
+ message: `API connection failed: ${err.message}`,
105
+ };
106
+ }
98
107
  }
99
108
 
100
- // 5. auth_credential 检查
109
+ // 5. auth_credential check
101
110
  if (ctx.resolvedAuth.is_present) {
102
111
  checks.auth_credential = {
103
112
  status: 'pass',
@@ -106,11 +115,11 @@ async function handleDoctor(args, flags, ctx) {
106
115
  } else {
107
116
  checks.auth_credential = {
108
117
  status: 'warn',
109
- message: '当前未配置 Access Secret (如需使用 projects/sql/dashboards 命令需登录)',
118
+ message: 'No Access Secret configured (login required for advanced features)',
110
119
  };
111
120
  }
112
121
 
113
- // 6. email_binding 检查(读取本地状态机中的 bound_email 字段)
122
+ // 6. email_binding check
114
123
  if (localCfg && localCfg.bound_email) {
115
124
  checks.email_binding = {
116
125
  status: 'pass',
@@ -120,11 +129,11 @@ async function handleDoctor(args, flags, ctx) {
120
129
  hasWarn = true;
121
130
  checks.email_binding = {
122
131
  status: 'warn',
123
- message: '未检测到绑定邮箱,统计数据未激活。执行: shiplens auth bind --email <you@example.com>',
132
+ message: 'Email not bound. Run: shiplens auth bind --email <you@example.com>',
124
133
  };
125
134
  }
126
135
 
127
- // 7. schema_freshness 检查(本地 schema_last_modified_at 是否超过 7 天未同步)
136
+ // 7. schema_freshness check
128
137
  if (localCfg && localCfg.schema_last_modified_at) {
129
138
  const ageMs = Date.now() - localCfg.schema_last_modified_at * 1000;
130
139
  const ageDays = ageMs / (1000 * 60 * 60 * 24);
@@ -133,7 +142,7 @@ async function handleDoctor(args, flags, ctx) {
133
142
  checks.schema_freshness = {
134
143
  status: 'warn',
135
144
  age_days: Math.floor(ageDays),
136
- message: `本地 Schema ${Math.floor(ageDays)} 天未同步,建议执行 shiplens projects list 刷新`,
145
+ message: `Local schema is ${Math.floor(ageDays)} days old. Run shiplens projects list to refresh.`,
137
146
  };
138
147
  } else {
139
148
  checks.schema_freshness = {
@@ -145,7 +154,7 @@ async function handleDoctor(args, flags, ctx) {
145
154
  } else {
146
155
  checks.schema_freshness = {
147
156
  status: 'warn',
148
- message: '本地未记录 schema_last_modified_at,请重新执行 shiplens init projects list',
157
+ message: 'No schema_last_modified_at recorded locally. Run shiplens init or projects list.',
149
158
  };
150
159
  hasWarn = true;
151
160
  }
@@ -165,7 +174,7 @@ async function handleDoctor(args, flags, ctx) {
165
174
  };
166
175
 
167
176
  ctx.output(report, () => {
168
- console.log('🏥 Shiplens 项目环境与链路体检报告');
177
+ console.log('🏥 Shiplens Environment & Telemetry Diagnostic Report');
169
178
  console.log('='.repeat(50));
170
179
  for (const [name, item] of Object.entries(checks)) {
171
180
  let icon = '✅';
@@ -173,18 +182,18 @@ async function handleDoctor(args, flags, ctx) {
173
182
  else if (item.status === 'fail') icon = '❌';
174
183
 
175
184
  console.log(`${icon} [${name}]: ${item.status}`);
176
- if (item.app_id) console.log(` app_id: ${item.app_id}`);
177
- if (item.email) console.log(` 邮箱: ${item.email}`);
178
- if (item.version) console.log(` 版本: ${item.version}`);
179
- if (item.file) console.log(` 文件: ${item.file}`);
180
- if (item.latency_ms !== undefined) console.log(` 网络延迟: ${item.latency_ms} ms`);
181
- if (item.source) console.log(` 凭据来源: ${item.source}`);
182
- if (item.age_days !== undefined) console.log(` Schema 距上次同步: ${item.age_days} 天`);
183
- if (item.message) console.log(` 说明: ${item.message}`);
185
+ if (item.app_id) console.log(` App ID: ${item.app_id}`);
186
+ if (item.email) console.log(` Email: ${item.email}`);
187
+ if (item.version) console.log(` Version: ${item.version}`);
188
+ if (item.file) console.log(` File: ${item.file}`);
189
+ if (item.latency_ms !== undefined) console.log(` Latency: ${item.latency_ms} ms`);
190
+ if (item.source) console.log(` Credential Source: ${item.source}`);
191
+ if (item.age_days !== undefined) console.log(` Schema Age: ${item.age_days} day(s)`);
192
+ if (item.message) console.log(` Details: ${item.message}`);
184
193
  }
185
194
  console.log('-'.repeat(50));
186
- console.log(`🩺 总体体检结果: ${overall}`);
187
- console.log(`⏱ 体检时间: ${report.doctor_ran_at}`);
195
+ console.log(`🩺 Overall Status: ${overall}`);
196
+ console.log(`⏱ Timestamp: ${report.doctor_ran_at}`);
188
197
  });
189
198
  }
190
199