@shiplens/cli 1.4.2 → 1.4.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.
@@ -2,28 +2,33 @@ async function handleHeatmap(args, flags, ctx) {
2
2
  const appId = ctx.resolveAppId();
3
3
  const templateId = flags.template || args[0];
4
4
  if (!templateId) {
5
- throw new Error('请通过 --template <id> 指定目标页面模板 ID');
5
+ throw new Error('Specify target page template ID via --template <id>');
6
6
  }
7
7
 
8
8
  const env = flags.env || 'production';
9
9
  const domHash = flags['dom-hash'] || '';
10
10
 
11
- const resp = await ctx.client.heatmap(appId, templateId, env, domHash);
11
+ const resp = await ctx.client.getHeatmap({
12
+ app_id: appId,
13
+ template_id: templateId,
14
+ env,
15
+ dom_hash: domHash,
16
+ });
12
17
 
13
18
  ctx.output(resp, () => {
14
- console.log(`🔥 页面热力图分析 (模板: ${resp.template_id}, DOM 哈希: ${resp.dom_hash || '-'}):`);
19
+ console.log(`🔥 Page Heatmap Analysis (Template: ${resp.template_id}, DOM Hash: ${resp.dom_hash || '-'}):`);
15
20
  if (resp.skeleton_svg_url) {
16
- console.log(`🖼️ 骨架图 SVG 预览地址: ${resp.skeleton_svg_url}`);
21
+ console.log(`🖼️ Skeleton SVG Preview URL: ${resp.skeleton_svg_url}`);
17
22
  }
18
- console.log(`👆 总点击数: ${resp.clicks_total || 0}\n`);
23
+ console.log(`👆 Total Clicks: ${resp.clicks_total || 0}\n`);
19
24
  if (resp.elements && resp.elements.length > 0) {
20
- console.log('高频点击元素分布:');
25
+ console.log('Top Clicked Elements:');
21
26
  resp.elements.forEach((el, i) => {
22
- console.log(`[${i + 1}] 元素: ${el.label || '-'} (选择器: ${el.selector})`);
23
- console.log(` 点击数: ${el.clicks} | 点击转化率: ${((el.click_rate || 0) * 100).toFixed(2)}% | 坐标: (${el.x_pct}, ${el.y_pct})`);
27
+ console.log(`[${i + 1}] Element: ${el.label || '-'} (Selector: ${el.selector})`);
28
+ console.log(` Clicks: ${el.clicks} | Click Rate: ${((el.click_rate || 0) * 100).toFixed(2)}% | Coordinates: (${el.x_pct}, ${el.y_pct})`);
24
29
  });
25
30
  } else {
26
- console.log('(当前模板暂无点击元素数据)');
31
+ console.log('(No click data found for this template)');
27
32
  }
28
33
  });
29
34
  }
@@ -13,7 +13,7 @@ async function handleInit(args, flags, ctx) {
13
13
  const description = flags.description || detected.description || '';
14
14
  const framework = flags.framework || detected.framework;
15
15
 
16
- // 4 级分类判定:优先命令行显式参数,否则使用 detectProject 智能推断结果
16
+ // Taxonomy resolution
17
17
  let taxonomy = detected.taxonomy;
18
18
  if (flags.genre || flags.subgenre || flags.tags) {
19
19
  const rawTags = flags.tags ? (Array.isArray(flags.tags) ? flags.tags : flags.tags.split(',').map((t) => t.trim())) : (taxonomy ? taxonomy.feature_tag_ids : []);
@@ -24,20 +24,20 @@ async function handleInit(args, flags, ctx) {
24
24
  const subgenreId = taxonomy?.subgenre?.id || 'browser_web_utility';
25
25
  const featureTagIds = taxonomy?.feature_tag_ids || [];
26
26
  if (featureTagIds.length > 10) {
27
- throw new Error('最多只能提交 10 个四级特性标签');
27
+ throw new Error('Maximum 10 feature tags allowed.');
28
28
  }
29
29
  const industry = flags.industry || subgenreId || 'saas';
30
30
 
31
- // 1. 检查本地是否已存在 SDK 或已有项目编号 (防意外覆盖分支)
31
+ // 1. Safety check for existing project configuration
32
32
  const existing = detectExistingApp(wd);
33
33
  if (existing.has_existing && !flags.force) {
34
34
  if (ctx.isJSON) {
35
35
  const result = {
36
36
  ok: false,
37
37
  code: 'PROJECT_EXISTS_LOCALLY',
38
- message: `检测到本地已存在 Shiplens 统计配置 (项目 ID: ${existing.app_id}),位于 ${existing.source_file}。` +
39
- `选项 1 [推荐]: 保持现有统计,保留原项目编号与历史数据;` +
40
- `选项 2: 重新创建并覆盖 (追加 --force,向云端申请全新项目编号并覆盖本地,旧看板将停止接收新数据)。`,
38
+ message: `Existing Shiplens configuration detected (App ID: ${existing.app_id}) in ${existing.source_file}. ` +
39
+ `Option 1 [Recommended]: Keep existing statistics and project ID. ` +
40
+ `Option 2: Use --force to overwrite and request a new App ID from cloud (Note: Old dashboard stops receiving new data).`,
41
41
  existing_app_id: existing.app_id,
42
42
  source_file: existing.source_file,
43
43
  project_name: existing.project_name || projectName,
@@ -47,44 +47,44 @@ async function handleInit(args, flags, ctx) {
47
47
  process.exitCode = 1;
48
48
  return;
49
49
  } else if (!process.stdin.isTTY) {
50
- console.log(`\n⚠️ 检测到当前项目已存在 Shiplens 配置!`);
51
- console.log(`📌 本地已存在项目编号: ${existing.app_id} (来源: ${existing.source_file})`);
52
- console.log(`📊 现有数据看板: ${ctx.client.baseURL}/dashboard/${existing.app_id}`);
53
- console.log(`🛑 检测到非交互式终端环境 (CI/Agent),已自动保留现有配置。`);
54
- console.log(`💡 选项 1 [推荐]: 保持现有统计,保留原项目 ID 与历史数据;`);
55
- console.log(`💡 选项 2: 若要强制重新创建新项目并覆盖本地配置,请追加 --force 参数:`);
50
+ console.log(`\n⚠️ Existing Shiplens project detected!`);
51
+ console.log(`📌 App ID: ${existing.app_id} (Source: ${existing.source_file})`);
52
+ console.log(`📊 Dashboard: ${ctx.client.baseURL}/dashboard/${existing.app_id}`);
53
+ console.log(`🛑 Non-interactive terminal detected (CI/Agent). Existing configuration preserved.`);
54
+ console.log(`💡 Option 1 [Recommended]: Keep existing setup, retain App ID and historical statistics;`);
55
+ console.log(`💡 Option 2: Overwrite with --force to create a new project:`);
56
56
  console.log(` npx.cmd --yes @shiplens/cli init --force\n`);
57
57
  return;
58
58
  } else {
59
- console.log(`\n⚠️ 检测到当前项目可能正在统计中!`);
60
- console.log(`📌 本地已存在项目编号: ${existing.app_id} (来源: ${existing.source_file})`);
61
- console.log(`📊 现有数据看板: ${ctx.client.baseURL}/dashboard/${existing.app_id}`);
62
- console.log(`\n💡 选项 1 [推荐]: 保持现有统计,继续使用原项目 ID,历史数据和现有看板完整保留;`);
63
- console.log(`💡 选项 2: 强制重新创建并覆盖 (追加 --force),申请全新空白项目 ID 覆盖本地 (注意:旧看板停止接收新数据,新旧数据无法合并):`);
59
+ console.log(`\n⚠️ Existing Shiplens project detected!`);
60
+ console.log(`📌 App ID: ${existing.app_id} (Source: ${existing.source_file})`);
61
+ console.log(`📊 Dashboard: ${ctx.client.baseURL}/dashboard/${existing.app_id}`);
62
+ console.log(`\n💡 Option 1 [Recommended]: Keep existing setup, retain App ID and historical statistics;`);
63
+ console.log(`💡 Option 2: Overwrite with --force to request a brand new App ID from cloud (Note: Old dashboard stops receiving new data, old and new data cannot be merged):`);
64
64
  console.log(` npx.cmd --yes @shiplens/cli init --force\n`);
65
65
 
66
66
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
67
67
  const confirmed = await new Promise((resolve) => {
68
- rl.question('是否确认覆盖并重新创建全新项目?(y/N): ', (ans) => {
68
+ rl.question('Overwrite and create a new project? (y/N): ', (ans) => {
69
69
  rl.close();
70
70
  resolve(ans.trim().toLowerCase() === 'y' || ans.trim().toLowerCase() === 'yes');
71
71
  });
72
72
  });
73
73
  if (!confirmed) {
74
- console.log('🛑 操作已取消,已保留原项目配置与统计。');
74
+ console.log('🛑 Operation cancelled. Existing configuration preserved.');
75
75
  return;
76
76
  }
77
77
  }
78
78
  }
79
79
 
80
- // 2. 自动释放/注入通用 AI Skill 技能书 (.agents/skills/shiplens/SKILL.md rules)
80
+ // 2. Inject AI Skill (.agents/skills/shiplens/SKILL.md & Agent rules)
81
81
  const skillFile = injectSkill(wd);
82
82
 
83
- // 3. 上报/同步到服务端注册项目
83
+ // 3. Register project on cloud
84
84
  let appId = '';
85
85
  let dashboardUrl = '';
86
86
  let accountStatus = 'not_logged_in_unlinked';
87
- let accountStatusText = '未登录 (首次安装默认状态或本地不存在有效凭据)';
87
+ let accountStatusText = 'Not Logged In (Default state after first installation or no valid local credentials)';
88
88
 
89
89
  try {
90
90
  const connResp = await ctx.client.connect({
@@ -102,24 +102,24 @@ async function handleInit(args, flags, ctx) {
102
102
  if (ctx.resolvedAuth && ctx.resolvedAuth.is_present) {
103
103
  if (connResp.account_linked !== false && connResp.linked !== false && !connResp.unlinked) {
104
104
  accountStatus = 'logged_in_linked';
105
- accountStatusText = '已登录 (项目已与账号关联)';
105
+ accountStatusText = 'Logged In (Project linked to account)';
106
106
  } else {
107
107
  accountStatus = 'logged_in_unlinked';
108
- accountStatusText = '已登录 (项目和账号未关联)';
108
+ accountStatusText = 'Logged In (Project not linked to account)';
109
109
  }
110
110
  } else {
111
111
  accountStatus = 'not_logged_in_unlinked';
112
- accountStatusText = '未登录 (首次安装默认状态或本地不存在有效凭据)';
112
+ accountStatusText = 'Not Logged In (Default state after first installation or no valid local credentials)';
113
113
  }
114
114
  } catch (netErr) {
115
115
  netErr.code = netErr.code || 'CONNECT_FAILED';
116
116
  throw netErr;
117
117
  }
118
118
 
119
- // 4. 自动插桩注入 SDK 代码
119
+ // 4. Inject SDK code
120
120
  const injectedFile = injectSDK(wd, framework, appId);
121
121
 
122
- // 5. 写入本地状态机 .shiplens.json
122
+ // 5. Save local configuration
123
123
  saveLocalConfig(wd, {
124
124
  app_id: appId,
125
125
  project_name: projectName,
@@ -132,17 +132,17 @@ async function handleInit(args, flags, ctx) {
132
132
  last_synced_at: new Date().toISOString(),
133
133
  });
134
134
 
135
- // 6. 安装依赖 (4-Tier 阶梯并发竞速)
135
+ // 6. Install SDK dependency
136
136
  let installResult = { success: true, manager: 'skipped' };
137
137
  if (!flags['no-install'] && detected.package_manager) {
138
138
  installResult = await installSDKDependency(wd, detected.package_manager);
139
139
  if (!installResult.success) {
140
- const errMsg = `SDK 安装失败: 无法从 NPM 官方源、备用镜像源、GitHub 源码镜像或官方 CDN 镜像下载安装 @shiplens/sdk。\n` +
141
- `排查步骤建议:\n` +
142
- ` 1. 检查本地网络连接或 HTTP 代理 / VPN 状态;\n` +
143
- ` 2. 尝试手动使用备用镜像安装: npm install @shiplens/sdk --registry=https://registry.npmmirror.com;\n` +
144
- ` 3. 或手动通过官方源安装: npm install @shiplens/sdk;\n` +
145
- ` 4. 访问 https://shiplens.dev 查看最新 SDK 状态与手动安装指南。`;
140
+ const errMsg = `SDK installation failed: Unable to download @shiplens/sdk across all download sources (official npm registry, backup mirror, GitHub mirror, and Shiplens CDN mirror).\n` +
141
+ `Troubleshooting steps:\n` +
142
+ ` 1. Check your local network connection or HTTP proxy / VPN settings;\n` +
143
+ ` 2. Try running manually with backup mirror: npm install @shiplens/sdk --registry=https://registry.npmmirror.com;\n` +
144
+ ` 3. Or install via official registry: npm install @shiplens/sdk;\n` +
145
+ ` 4. Visit https://shiplens.dev for latest SDK status and installation guides.`;
146
146
 
147
147
  if (ctx.isJSON) {
148
148
  ctx.output({
@@ -160,7 +160,7 @@ async function handleInit(args, flags, ctx) {
160
160
  }
161
161
  }
162
162
 
163
- // 7. 绑定邮箱 (若用户显式指定或传 auto)
163
+ // 7. Bind email if specified
164
164
  let targetEmail = flags.email;
165
165
  if (targetEmail === 'auto') {
166
166
  targetEmail = getGitEmail();
@@ -205,7 +205,7 @@ async function handleInit(args, flags, ctx) {
205
205
  auth_status: authStatus,
206
206
  shiplens_env_written: authStatus === 'magic_link_sent',
207
207
  activation_required: authStatus === 'pending',
208
- free_tier_info: '每月 50,000 事件 (5,000 独立访客) 永久免费统计额度',
208
+ free_tier_info: '50,000 free events/month (~5,000 unique visitors)',
209
209
  bound_email: targetEmail || undefined,
210
210
  elapsed_ms: elapsed,
211
211
  atomic_completed: true,
@@ -213,31 +213,31 @@ async function handleInit(args, flags, ctx) {
213
213
 
214
214
  ctx.output(result, () => {
215
215
  console.log(`\n==================================================`);
216
- console.log(`✅ Shiplens 数据统计 SDK 已成功安装并完成代码插桩!(耗时 ${elapsed} ms)\n`);
217
- console.log(`📦 项目与数据看板信息`);
218
- console.log(`项目名称: ${projectName}`);
216
+ console.log(`✅ Shiplens SDK successfully integrated (${elapsed} ms)\n`);
217
+ console.log(`📦 Project & Dashboard Information`);
218
+ console.log(`Project Name: ${projectName}`);
219
219
  if (description) {
220
- console.log(`项目详情: ${description}`);
220
+ console.log(`Description: ${description}`);
221
221
  }
222
222
  if (taxonomyFormatted) {
223
- console.log(`项目类别:`);
223
+ console.log(`Taxonomy:`);
224
224
  console.log(taxonomyFormatted);
225
225
  }
226
- console.log(`应用 ID (App ID): ${appId}`);
227
- console.log(`代码注入: 已自动在入口文件 ${injectedFile} 中配置集成 (框架: ${framework})`);
228
- console.log(`实时数据看板 / 项目激活链接: 🔗 ${dashboardUrl}`);
229
- console.log(`用户个人账号: ${accountStatusText}`);
230
- console.log(`🧠 AI 技能就绪: ${skillFile}`);
231
- console.log(`🔒 [SHIPLENS_ATOMIC_COMPLETED] 所有操作已原子化闭环,无需执行 git status / doctor 等额外检查。`);
226
+ console.log(`App ID: ${appId}`);
227
+ console.log(`Code Injection: Automatically configured in ${injectedFile} (Framework: ${framework})`);
228
+ console.log(`Live Dashboard / Activation URL: 🔗 ${dashboardUrl}`);
229
+ console.log(`User Account: ${accountStatusText}`);
230
+ console.log(`🧠 AI Skill Ready: ${skillFile}`);
231
+ console.log(`🔒 [SHIPLENS_ATOMIC_COMPLETED] Setup completed atomically. No extra commands needed.`);
232
232
  console.log(`==================================================\n`);
233
233
 
234
234
  if (authStatus === 'magic_link_sent' && targetEmail) {
235
- console.log(`📧 激活邮件已发送至: ${targetEmail}`);
236
- console.log(`👉 请点击邮件中的链接完成注册与设备授权,即可永久激活每月 50,000 事件免费统计额度。`);
235
+ console.log(`📧 Activation email sent to: ${targetEmail}`);
236
+ console.log(`👉 Click the link in your email to activate your 50,000 monthly free events quota and authorize this device.`);
237
237
  } else {
238
238
  console.log(`> [!IMPORTANT]`);
239
- console.log(`> **项目待激活(数据统计尚未生效)**`);
240
- console.log(`> **请告诉我您的邮箱**:点击邮件中的链接即可完成注册,激活每月 50,000 事件的免费额度(约5000人访问),同时绑定项目和账号;激活后您与我直接对话,我就有权限查询并分析产品数据。`);
239
+ console.log(`> **Project Pending Activation (Analytics not active yet)**`);
240
+ console.log(`> **Please share your email**: Click the link in the email to complete registration, activate 50,000 free monthly events (~5,000 visitors), and bind your project to your account. Once activated, you can chat directly with me to query and analyze product data.`);
241
241
  }
242
242
  });
243
243
  }
@@ -39,9 +39,9 @@ function emitResponse(result) {
39
39
  }
40
40
 
41
41
  async function handleMcp(subcommand) {
42
- if (subcommand !== 'serve') throw new Error('支持的 mcp 子命令: serve');
42
+ if (subcommand !== 'serve') throw new Error('Supported mcp subcommand: serve');
43
43
  const deviceEnv = getDeviceEnv();
44
- if (!deviceEnv) throw new Error('未找到 shiplens.env,请先执行 shiplens auth bind 并完成邮件激活');
44
+ if (!deviceEnv) throw new Error('shiplens.env not found. Run shiplens auth bind and complete email activation first.');
45
45
  let buffer = '';
46
46
  let sessionId = '';
47
47
  process.stdin.setEncoding('utf8');
@@ -4,23 +4,22 @@ async function handlePages(args, flags, ctx) {
4
4
  const env = flags.env || 'production';
5
5
  const limit = parseInt(flags.limit || '10', 10);
6
6
 
7
- const resp = await ctx.client.pages(appId, range, env, limit);
7
+ const resp = await ctx.client.getPages({ app_id: appId, range, env, limit });
8
8
 
9
9
  ctx.output(resp, () => {
10
- console.log(`📄 页面证据分析 (时间范围: ${range}, ${limit}):\n`);
10
+ console.log(`📄 Page Telemetry Evidence (Range: ${range}, Top ${limit}):\n`);
11
11
  const pages = Array.isArray(resp.pages) ? resp.pages : (Array.isArray(resp) ? resp : []);
12
12
  if (pages.length === 0) {
13
- console.log('(暂无页面数据,请确认 SDK 已正常上报)');
13
+ console.log('(No page data found. Verify SDK reporting.)');
14
14
  return;
15
15
  }
16
- // 表格化输出
17
- console.log('序号 | 路径/标题 | PV | UV | 平均停留(s)');
18
- console.log('--- | ---------------------------------- | ------- | ------- | ----------');
16
+ console.log('# | Path / Title | PV | UV | Avg Dwell(s)');
17
+ console.log('--- | ---------------------------------- | ------- | ------- | ------------');
19
18
  pages.forEach((p, i) => {
20
19
  const path = (p.path || p.template_id || '-').padEnd(34);
21
20
  const pv = String(p.pv || p.pageviews || 0).padEnd(7);
22
21
  const uv = String(p.uv || p.unique_visitors || 0).padEnd(7);
23
- const dur = String(p.avg_duration_s || p.avg_session_duration_s || 0).padEnd(10);
22
+ const dur = String(p.avg_duration_s || p.avg_session_duration_s || 0).padEnd(12);
24
23
  console.log(`[${String(i + 1).padEnd(2)}] | ${path} | ${pv} | ${uv} | ${dur}`);
25
24
  });
26
25
  });
@@ -31,30 +30,27 @@ async function handlePaths(args, flags, ctx) {
31
30
  const range = flags.range || '7d';
32
31
  const env = flags.env || 'production';
33
32
 
34
- const resp = await ctx.client.paths(appId, range, env);
33
+ const resp = await ctx.client.queryPaths({ app_id: appId, range, env });
35
34
 
36
35
  ctx.output(resp, () => {
37
- console.log(`🔀 页面流转路径分析 (时间范围: ${range}):\n`);
36
+ console.log(`🔀 User Flow & Path Analysis (Range: ${range}):\n`);
38
37
 
39
- // 入口页面
40
38
  if (resp.entry_pages && resp.entry_pages.length > 0) {
41
- console.log('▶ 主要入口页面:');
39
+ console.log('▶ Top Entry Pages:');
42
40
  resp.entry_pages.slice(0, 5).forEach((e, i) =>
43
- console.log(` [${i + 1}] ${e.path || e.page} ${e.sessions || e.count || 0} 次会话`)
41
+ console.log(` [${i + 1}] ${e.path || e.page} - ${e.sessions || e.count || 0} sessions`)
44
42
  );
45
43
  }
46
- // 退出页面
47
44
  if (resp.exit_pages && resp.exit_pages.length > 0) {
48
- console.log('\n◀ 主要退出页面:');
45
+ console.log('\n◀ Top Exit Pages:');
49
46
  resp.exit_pages.slice(0, 5).forEach((e, i) =>
50
- console.log(` [${i + 1}] ${e.path || e.page} 退出率 ${((e.exit_rate || 0) * 100).toFixed(1)}%`)
47
+ console.log(` [${i + 1}] ${e.path || e.page} - Exit Rate ${((e.exit_rate || 0) * 100).toFixed(1)}%`)
51
48
  );
52
49
  }
53
- // 路径流转
54
50
  if (resp.paths && resp.paths.length > 0) {
55
- console.log('\n↔ 高频页面流转路径:');
51
+ console.log('\n↔ High-Frequency Paths:');
56
52
  resp.paths.slice(0, 8).forEach((p, i) =>
57
- console.log(` [${i + 1}] ${p.from || p.source} → ${p.to || p.target} (${p.count || p.sessions || 0} )`)
53
+ console.log(` [${i + 1}] ${p.from || p.source} → ${p.to || p.target} (${p.count || p.sessions || 0} times)`)
58
54
  );
59
55
  } else {
60
56
  console.log(JSON.stringify(resp, null, 2));
@@ -67,18 +63,18 @@ async function handleCanvas(args, flags, ctx) {
67
63
  const range = flags.range || '7d';
68
64
  const env = flags.env || 'production';
69
65
 
70
- const resp = await ctx.client.behaviorCanvas(appId, range, env);
66
+ const resp = await ctx.client.queryPaths({ app_id: appId, range, env, mode: 'canvas' });
71
67
 
72
68
  ctx.output(resp, () => {
73
- console.log(`🎨 用户行为画布拓扑 (时间范围: ${range}):\n`);
69
+ console.log(`🎨 User Behavior Canvas Topology (Range: ${range}):\n`);
74
70
  const nodeCount = (resp.nodes || []).length;
75
71
  const edgeCount = (resp.edges || resp.links || []).length;
76
72
  if (nodeCount > 0 || edgeCount > 0) {
77
- console.log(`📍 ${nodeCount} 个节点, ${edgeCount} 条边`);
73
+ console.log(`📍 ${nodeCount} nodes, ${edgeCount} edges`);
78
74
  if (resp.nodes && resp.nodes.length > 0) {
79
- console.log('\n主要节点:');
75
+ console.log('\nMain Nodes:');
80
76
  resp.nodes.slice(0, 10).forEach((n, i) =>
81
- console.log(` [${i + 1}] ${n.label || n.path || n.id} (访问量: ${n.value || n.count || 0})`)
77
+ console.log(` [${i + 1}] ${n.label || n.path || n.id} (Visits: ${n.value || n.count || 0})`)
82
78
  );
83
79
  }
84
80
  } else {
@@ -9,19 +9,19 @@ async function handleProjects(subcommand, args, flags, ctx) {
9
9
  ctx.output(resp, () => {
10
10
  const projects = resp.projects || [];
11
11
  if (projects.length === 0) {
12
- console.log('暂无关联项目。可使用 shiplens init 初始化一个新项目。');
12
+ console.log('No associated projects found. Use shiplens init to create a new project.');
13
13
  return;
14
14
  }
15
- console.log(`📦 共有 ${projects.length} 个项目:\n`);
15
+ console.log(`📦 Found ${projects.length} project(s):\n`);
16
16
  projects.forEach((p, i) => {
17
17
  console.log(`[${i + 1}] ${p.project_name || p.app_name || 'Untitled'} (${p.app_id})`);
18
- if (p.description) console.log(` 简介: ${p.description}`);
18
+ if (p.description) console.log(` Description: ${p.description}`);
19
19
  if (p.taxonomy?.subgenre?.name) {
20
20
  const tags = (p.taxonomy.feature_tags || []).map((tag) => tag.name).join(', ');
21
- console.log(` 分类: ${p.taxonomy.genre?.name || '-'} / ${p.taxonomy.subgenre.name}${tags ? ` · ${tags}` : ''}`);
21
+ console.log(` Taxonomy: ${p.taxonomy.genre?.name || '-'} / ${p.taxonomy.subgenre.name}${tags ? ` - ${tags}` : ''}`);
22
22
  }
23
- console.log(` 行业: ${p.industry || '-'} | 状态: ${p.status || 'active'} | Schema更新时间戳: ${p.schema_last_modified_at || 0}`);
24
- if (p.created_at) console.log(` 创建时间: ${p.created_at}`);
23
+ console.log(` Industry: ${p.industry || '-'} | Status: ${p.status || 'active'} | Schema Timestamp: ${p.schema_last_modified_at || 0}`);
24
+ if (p.created_at) console.log(` Created: ${p.created_at}`);
25
25
  console.log('');
26
26
  });
27
27
  });
@@ -32,7 +32,7 @@ async function handleProjects(subcommand, args, flags, ctx) {
32
32
  const appId = ctx.resolveAppId();
33
33
  const res = await ctx.client.bindProject(appId, flags.name);
34
34
  ctx.output(res, () => {
35
- console.log(`✅ 项目 ${appId} 已成功绑定至当前账号!`);
35
+ console.log(`✅ Project ${appId} bound to current account successfully.`);
36
36
  });
37
37
  break;
38
38
  }
@@ -40,36 +40,36 @@ async function handleProjects(subcommand, args, flags, ctx) {
40
40
  case 'delete': {
41
41
  const appId = ctx.resolveAppId();
42
42
  if (!flags.force) {
43
- console.log(`⚠️ 警告:正在删除项目 ${appId} 及其全部历史事件数据。该操作不可逆!`);
43
+ console.log(`⚠️ Warning: Deleting project ${appId} and all historical event data. This action cannot be undone!`);
44
44
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
45
45
  const confirmed = await new Promise((resolve) => {
46
- rl.question(`请输入项目 ID 'yes' 确认删除: `, (ans) => {
46
+ rl.question(`Enter App ID or 'yes' to confirm deletion: `, (ans) => {
47
47
  rl.close();
48
48
  resolve(ans.trim() === 'yes' || ans.trim() === appId);
49
49
  });
50
50
  });
51
51
  if (!confirmed) {
52
- console.log('🛑 操作已取消。');
52
+ console.log('🛑 Operation cancelled.');
53
53
  return;
54
54
  }
55
55
  }
56
56
 
57
57
  const res = await ctx.client.deleteProject(appId);
58
58
 
59
- // 清理本地专属业务上下文文件
59
+ // Clean up local business context file
60
60
  const localContextPath = path.join(process.cwd(), '.shiplens', 'contexts', `${appId}.md`);
61
61
  if (fs.existsSync(localContextPath)) {
62
62
  try { fs.unlinkSync(localContextPath); } catch {}
63
63
  }
64
64
 
65
65
  ctx.output(res, () => {
66
- console.log(`🗑️ 项目 ${appId} 已被成功删除。`);
66
+ console.log(`🗑️ Project ${appId} deleted successfully.`);
67
67
  });
68
68
  break;
69
69
  }
70
70
 
71
71
  default:
72
- throw new Error(`未知的 projects 子命令: ${subcommand}。支持 list, bind, delete`);
72
+ throw new Error(`Unknown projects subcommand: ${subcommand}. Supported: list, bind, delete`);
73
73
  }
74
74
  }
75
75
 
@@ -11,29 +11,28 @@ async function handleQuery(args, flags, ctx) {
11
11
 
12
12
  let queryReq;
13
13
 
14
- // 支持 --file:从 JSON 文件读取完整 AnalyticsQueryRequest,最大灵活度
14
+ // Support --file: read full AnalyticsQueryRequest from JSON file
15
15
  if (flags.file) {
16
16
  if (!fs.existsSync(flags.file)) {
17
- const err = new Error(`查询配置文件不存在: ${flags.file}`);
17
+ const err = new Error(`Query config file not found: ${flags.file}`);
18
18
  err.code = 'INVALID_ARGS';
19
19
  throw err;
20
20
  }
21
21
  try {
22
22
  queryReq = JSON.parse(fs.readFileSync(flags.file, 'utf8'));
23
23
  } catch (e) {
24
- const err = new Error(`无法解析查询配置文件 (${flags.file}): ${e.message}`);
24
+ const err = new Error(`Failed to parse query config file (${flags.file}): ${e.message}`);
25
25
  err.code = 'INVALID_ARGS';
26
26
  throw err;
27
27
  }
28
- // 允许用命令行 range / env 覆盖文件中的值
29
28
  if (!queryReq.range) queryReq.range = range;
30
29
  if (!queryReq.env) queryReq.env = env;
31
30
  } else {
32
- // 支持 --metrics 多指标(逗号分隔)或单指标 --metric
31
+ // Support --metrics (comma-separated) or --metric
33
32
  const metricsRaw = flags.metrics || flags.metric || 'pageviews';
34
33
  const metricList = String(metricsRaw).split(',').map((m) => m.trim()).filter(Boolean);
35
34
 
36
- // 解析 --filter key=val(可多次指定)
35
+ // Parse --filter key=val
37
36
  const filters = {};
38
37
  const filterList = Array.isArray(flags.filter)
39
38
  ? flags.filter
@@ -43,13 +42,12 @@ async function handleQuery(args, flags, ctx) {
43
42
  if (k && v.length > 0) filters[k.trim()] = v.join('=').trim();
44
43
  }
45
44
 
46
- // 每个指标生成一个 panel
47
45
  queryReq = {
48
46
  env,
49
47
  range,
50
48
  panels: metricList.map((metric, idx) => ({
51
49
  panel_id: `panel_cli_${idx}`,
52
- title: `${metric} 趋势`,
50
+ title: `${metric} Trend`,
53
51
  metric,
54
52
  chart_type: chartType,
55
53
  time_grain: grain,
@@ -60,22 +58,22 @@ async function handleQuery(args, flags, ctx) {
60
58
  };
61
59
  }
62
60
 
63
- const resp = await ctx.client.queryAnalytics(appId, queryReq);
61
+ const resp = await ctx.client.query(appId, queryReq);
64
62
 
65
63
  ctx.output(resp, () => {
66
64
  const metricNames = (queryReq.panels || []).map((p) => p.metric).join(', ');
67
- console.log(`📊 查询指标: ${metricNames} | 时间范围: ${queryReq.range}`);
65
+ console.log(`📊 Query Metrics: ${metricNames} | Range: ${queryReq.range}`);
68
66
  if (resp.data && resp.data.length > 0) {
69
67
  resp.data.forEach((row) => console.log('•', JSON.stringify(row)));
70
68
  } else if (resp.panels && resp.panels.length > 0) {
71
69
  resp.panels.forEach((p) => {
72
- console.log(`\n📈 面板: ${p.title || p.panel_id}`);
70
+ console.log(`\n📈 Panel: ${p.title || p.panel_id}`);
73
71
  if (Array.isArray(p.data)) {
74
72
  p.data.forEach((row) => console.log(' •', JSON.stringify(row)));
75
73
  }
76
74
  });
77
75
  } else {
78
- console.log('(暂无匹配数据)');
76
+ console.log('(No matching data found)');
79
77
  }
80
78
  });
81
79
  }
@@ -2,7 +2,7 @@ async function handleSQL(args, flags, ctx) {
2
2
  const appId = ctx.resolveAppId();
3
3
  let queryStr = flags.query || flags.sql || args.join(' ');
4
4
 
5
- // 支持 --stdin:从标准输入读取 SQL(兼容管道,如 echo "SELECT..." | shiplens sql --stdin)
5
+ // Support --stdin for piped SQL execution
6
6
  if (flags.stdin || flags['read-stdin']) {
7
7
  const chunks = [];
8
8
  for await (const chunk of process.stdin) {
@@ -13,19 +13,18 @@ async function handleSQL(args, flags, ctx) {
13
13
  }
14
14
 
15
15
  if (!queryStr) {
16
- const err = new Error('SQL 查询语句不能为空,请使用 --query "SELECT ..." 或通过管道传入 --stdin');
16
+ const err = new Error('SQL query string cannot be empty. Pass --query "SELECT ..." or pipe via --stdin.');
17
17
  err.code = 'INVALID_SQL';
18
18
  throw err;
19
19
  }
20
20
 
21
- const resp = await ctx.client.executeSQL(appId, queryStr);
21
+ const resp = await ctx.client.executeSQL({ app_id: appId, query: queryStr });
22
22
 
23
23
  ctx.output(resp, () => {
24
24
  const rowCount = resp.row_count !== undefined ? resp.row_count : (resp.rows ? resp.rows.length : 0);
25
- console.log(`🔍 SQL 执行成功 (耗时: ${resp.elapsed_ms || 0} ms, 返回 ${rowCount} ):\n`);
25
+ console.log(`🔍 SQL executed successfully (${resp.elapsed_ms || 0} ms, ${rowCount} rows returned):\n`);
26
26
 
27
27
  if (resp.columns && resp.columns.length > 0) {
28
- // 计算每列宽度,对齐输出
29
28
  const colWidths = resp.columns.map((col) => {
30
29
  let maxW = col.length;
31
30
  if (resp.rows) {
@@ -6,16 +6,16 @@ async function handleSummary(args, flags, ctx) {
6
6
  const resp = await ctx.client.summary(appId, range, env);
7
7
 
8
8
  ctx.output(resp, () => {
9
- console.log(`📈 产品核心数据摘要 (时间范围: ${range}):`);
10
- console.log(`• 总浏览量 (PV): ${resp.total_pv || 0}`);
11
- console.log(`• 独立访客 (UV): ${resp.total_uv || 0}`);
12
- console.log(`• 平均会话时长: ${resp.avg_session_duration_s || 0} 秒`);
13
- console.log(`• 跳出率: ${((resp.bounce_rate || 0) * 100).toFixed(1)}%`);
9
+ console.log(`📈 Core Product Summary (Range: ${range}):`);
10
+ console.log(`• Total Pageviews (PV): ${resp.total_pv || 0}`);
11
+ console.log(`• Unique Visitors (UV): ${resp.total_uv || 0}`);
12
+ console.log(`• Avg Session Duration: ${resp.avg_session_duration_s || 0} s`);
13
+ console.log(`• Bounce Rate: ${((resp.bounce_rate || 0) * 100).toFixed(1)}%`);
14
14
  if (resp.top_countries && resp.top_countries.length > 0) {
15
- console.log('• Top 国家/地区:', resp.top_countries);
15
+ console.log('• Top Countries:', resp.top_countries);
16
16
  }
17
17
  if (resp.top_devices && resp.top_devices.length > 0) {
18
- console.log('• Top 访问设备:', resp.top_devices);
18
+ console.log('• Top Devices:', resp.top_devices);
19
19
  }
20
20
  });
21
21
  }