@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.
- package/README.md +30 -30
- package/docs/02_CLI_COMMAND_REFERENCE.md +204 -0
- package/lib/api.js +35 -55
- package/lib/assets/actions.json +754 -0
- package/lib/assets/skill.js +137 -140
- package/lib/cli.js +64 -55
- package/lib/commands/action.js +112 -0
- package/lib/commands/auth.js +36 -41
- package/lib/commands/context.js +13 -18
- package/lib/commands/dashboards.js +16 -24
- package/lib/commands/doctor.js +43 -34
- package/lib/commands/heatmap.js +14 -9
- package/lib/commands/init.js +51 -51
- package/lib/commands/mcp.js +2 -2
- package/lib/commands/pages.js +19 -23
- package/lib/commands/projects.js +13 -13
- package/lib/commands/query.js +10 -12
- package/lib/commands/sql.js +4 -5
- package/lib/commands/summary.js +7 -7
- package/lib/injector.js +19 -22
- package/lib/mcp-config.js +5 -5
- package/lib/taxonomy.js +22 -25
- package/package.json +6 -3
- package/prompts/README.md +2 -3
- package/prompts/prompts_cli_en-US.md +821 -0
- package/docs/02_SHIPLENS_CLI_/345/205/250/345/221/275/344/273/244/345/217/202/350/200/203/346/211/213/345/206/214.md +0 -205
- package/prompts/prompts_cli_zh.md +0 -743
package/lib/commands/heatmap.js
CHANGED
|
@@ -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('
|
|
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.
|
|
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(`🔥
|
|
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(`🖼️
|
|
21
|
+
console.log(`🖼️ Skeleton SVG Preview URL: ${resp.skeleton_svg_url}`);
|
|
17
22
|
}
|
|
18
|
-
console.log(`👆
|
|
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}]
|
|
23
|
-
console.log(`
|
|
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
|
}
|
package/lib/commands/init.js
CHANGED
|
@@ -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
|
-
//
|
|
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('
|
|
27
|
+
throw new Error('Maximum 10 feature tags allowed.');
|
|
28
28
|
}
|
|
29
29
|
const industry = flags.industry || subgenreId || 'saas';
|
|
30
30
|
|
|
31
|
-
// 1.
|
|
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:
|
|
39
|
-
|
|
40
|
-
|
|
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⚠️
|
|
51
|
-
console.log(`📌
|
|
52
|
-
console.log(`📊
|
|
53
|
-
console.log(`🛑
|
|
54
|
-
console.log(`💡
|
|
55
|
-
console.log(`💡
|
|
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(`📌
|
|
61
|
-
console.log(`📊
|
|
62
|
-
console.log(`\n💡
|
|
63
|
-
console.log(`💡
|
|
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('
|
|
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.
|
|
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.
|
|
119
|
+
// 4. Inject SDK code
|
|
120
120
|
const injectedFile = injectSDK(wd, framework, appId);
|
|
121
121
|
|
|
122
|
-
// 5.
|
|
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.
|
|
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
|
|
141
|
-
|
|
142
|
-
` 1.
|
|
143
|
-
` 2.
|
|
144
|
-
` 3.
|
|
145
|
-
` 4.
|
|
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.
|
|
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: '
|
|
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
|
|
217
|
-
console.log(`📦
|
|
218
|
-
console.log(
|
|
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(
|
|
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(
|
|
227
|
-
console.log(
|
|
228
|
-
console.log(
|
|
229
|
-
console.log(
|
|
230
|
-
console.log(`🧠 AI
|
|
231
|
-
console.log(`🔒 [SHIPLENS_ATOMIC_COMPLETED]
|
|
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(`📧
|
|
236
|
-
console.log(`👉
|
|
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(`>
|
|
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
|
}
|
package/lib/commands/mcp.js
CHANGED
|
@@ -39,9 +39,9 @@ function emitResponse(result) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
async function handleMcp(subcommand) {
|
|
42
|
-
if (subcommand !== 'serve') throw new Error('
|
|
42
|
+
if (subcommand !== 'serve') throw new Error('Supported mcp subcommand: serve');
|
|
43
43
|
const deviceEnv = getDeviceEnv();
|
|
44
|
-
if (!deviceEnv) throw new Error('
|
|
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');
|
package/lib/commands/pages.js
CHANGED
|
@@ -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.
|
|
7
|
+
const resp = await ctx.client.getPages({ app_id: appId, range, env, limit });
|
|
8
8
|
|
|
9
9
|
ctx.output(resp, () => {
|
|
10
|
-
console.log(`📄
|
|
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('
|
|
13
|
+
console.log('(No page data found. Verify SDK reporting.)');
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
console.log('
|
|
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(
|
|
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.
|
|
33
|
+
const resp = await ctx.client.queryPaths({ app_id: appId, range, env });
|
|
35
34
|
|
|
36
35
|
ctx.output(resp, () => {
|
|
37
|
-
console.log(`🔀
|
|
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}
|
|
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}
|
|
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.
|
|
66
|
+
const resp = await ctx.client.queryPaths({ app_id: appId, range, env, mode: 'canvas' });
|
|
71
67
|
|
|
72
68
|
ctx.output(resp, () => {
|
|
73
|
-
console.log(`🎨
|
|
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(`📍
|
|
73
|
+
console.log(`📍 ${nodeCount} nodes, ${edgeCount} edges`);
|
|
78
74
|
if (resp.nodes && resp.nodes.length > 0) {
|
|
79
|
-
console.log('\
|
|
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} (
|
|
77
|
+
console.log(` [${i + 1}] ${n.label || n.path || n.id} (Visits: ${n.value || n.count || 0})`)
|
|
82
78
|
);
|
|
83
79
|
}
|
|
84
80
|
} else {
|
package/lib/commands/projects.js
CHANGED
|
@@ -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('
|
|
12
|
+
console.log('No associated projects found. Use shiplens init to create a new project.');
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
|
-
console.log(`📦
|
|
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(`
|
|
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(`
|
|
21
|
+
console.log(` Taxonomy: ${p.taxonomy.genre?.name || '-'} / ${p.taxonomy.subgenre.name}${tags ? ` - ${tags}` : ''}`);
|
|
22
22
|
}
|
|
23
|
-
console.log(`
|
|
24
|
-
if (p.created_at) console.log(`
|
|
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(`✅
|
|
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(`⚠️
|
|
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(
|
|
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(`🗑️
|
|
66
|
+
console.log(`🗑️ Project ${appId} deleted successfully.`);
|
|
67
67
|
});
|
|
68
68
|
break;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
default:
|
|
72
|
-
throw new Error(
|
|
72
|
+
throw new Error(`Unknown projects subcommand: ${subcommand}. Supported: list, bind, delete`);
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
|
package/lib/commands/query.js
CHANGED
|
@@ -11,29 +11,28 @@ async function handleQuery(args, flags, ctx) {
|
|
|
11
11
|
|
|
12
12
|
let queryReq;
|
|
13
13
|
|
|
14
|
-
//
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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.
|
|
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(`📊
|
|
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📈
|
|
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
|
}
|
package/lib/commands/sql.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
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
|
|
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) {
|
package/lib/commands/summary.js
CHANGED
|
@@ -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(`📈
|
|
10
|
-
console.log(`•
|
|
11
|
-
console.log(`•
|
|
12
|
-
console.log(`•
|
|
13
|
-
console.log(`•
|
|
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
|
|
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
|
|
18
|
+
console.log('• Top Devices:', resp.top_devices);
|
|
19
19
|
}
|
|
20
20
|
});
|
|
21
21
|
}
|