@shiplens/cli 1.2.7
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 +89 -0
- package/bin/shiplens.js +5 -0
- package/lib/api.js +376 -0
- package/lib/assets/skill.js +127 -0
- package/lib/assets/taxonomy.json +5184 -0
- package/lib/auth.js +107 -0
- package/lib/cli.js +258 -0
- package/lib/commands/auth.js +242 -0
- package/lib/commands/context.js +185 -0
- package/lib/commands/dashboards.js +80 -0
- package/lib/commands/doctor.js +200 -0
- package/lib/commands/heatmap.js +36 -0
- package/lib/commands/init.js +244 -0
- package/lib/commands/mcp.js +65 -0
- package/lib/commands/pages.js +90 -0
- package/lib/commands/projects.js +76 -0
- package/lib/commands/query.js +81 -0
- package/lib/commands/sql.js +61 -0
- package/lib/commands/summary.js +23 -0
- package/lib/config.js +80 -0
- package/lib/device-env.js +16 -0
- package/lib/index.js +21 -0
- package/lib/injector.js +510 -0
- package/lib/mcp-config.js +96 -0
- package/lib/taxonomy.js +354 -0
- package/package.json +44 -0
- package/prompts/README.md +21 -0
- package/prompts/prompts_cli_en.md +671 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { getLocalConfig } = require('../config');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get standard storage path for business context: .shiplens/contexts/<app_id>.md
|
|
7
|
+
*/
|
|
8
|
+
function getContextFilePath(appId, customFile = null, dir = process.cwd()) {
|
|
9
|
+
if (customFile) return path.resolve(dir, customFile);
|
|
10
|
+
return path.join(dir, '.shiplens', 'contexts', `${appId}.md`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extract declared app_id from Markdown header
|
|
15
|
+
*/
|
|
16
|
+
function extractAppIdFromMarkdown(content) {
|
|
17
|
+
const match = content.match(/app_id[^\w\n]*[:=]\s*([a-zA-Z0-9_.\-]+)/i);
|
|
18
|
+
return match ? match[1].trim() : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Network executor with up to 2 retries and graceful fallback
|
|
23
|
+
*/
|
|
24
|
+
async function executeWithSafeFallback(fn, maxRetries = 2, baseMs = 500) {
|
|
25
|
+
let lastErr = null;
|
|
26
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
27
|
+
try {
|
|
28
|
+
return { success: true, result: await fn() };
|
|
29
|
+
} catch (err) {
|
|
30
|
+
lastErr = err;
|
|
31
|
+
if (attempt < maxRetries) {
|
|
32
|
+
await new Promise((r) => setTimeout(r, baseMs * attempt));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { success: false, attempts: maxRetries, error: lastErr };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Handle context command (push, pull, show)
|
|
41
|
+
*/
|
|
42
|
+
async function handleContext(subcommand, args, flags, ctx) {
|
|
43
|
+
const targetSub = subcommand || 'show';
|
|
44
|
+
const appId = ctx.resolveAppId();
|
|
45
|
+
const filePath = getContextFilePath(appId, flags.file);
|
|
46
|
+
const localCfg = getLocalConfig() || {};
|
|
47
|
+
const projectName = flags.name || localCfg.project_name || '';
|
|
48
|
+
|
|
49
|
+
switch (targetSub) {
|
|
50
|
+
case 'push': {
|
|
51
|
+
if (!fs.existsSync(filePath)) {
|
|
52
|
+
const displayPath = path.relative(process.cwd(), filePath) || filePath;
|
|
53
|
+
const err = new Error(`Local context file not found: ${displayPath}. Generate business context first.`);
|
|
54
|
+
err.code = 'FILE_NOT_FOUND';
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
59
|
+
const declaredAppId = extractAppIdFromMarkdown(content);
|
|
60
|
+
|
|
61
|
+
if (declaredAppId && declaredAppId !== appId) {
|
|
62
|
+
const err = new Error(`Context file app_id (${declaredAppId}) does not match current target app_id (${appId}). Push rejected.`);
|
|
63
|
+
err.code = 'PROJECT_ID_MISMATCH';
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const syncResult = await executeWithSafeFallback(async () => {
|
|
68
|
+
return ctx.client.uploadProjectContext(appId, {
|
|
69
|
+
context_markdown: content,
|
|
70
|
+
project_name: projectName,
|
|
71
|
+
});
|
|
72
|
+
}, 2, 500);
|
|
73
|
+
|
|
74
|
+
if (!syncResult.success) {
|
|
75
|
+
ctx.output({
|
|
76
|
+
ok: true,
|
|
77
|
+
skipped: true,
|
|
78
|
+
action: 'push',
|
|
79
|
+
app_id: appId,
|
|
80
|
+
file: filePath,
|
|
81
|
+
message: 'Server connection timed out (2 attempts). Skipped cloud sync without blocking.',
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
ctx.output({
|
|
87
|
+
ok: true,
|
|
88
|
+
action: 'push',
|
|
89
|
+
app_id: appId,
|
|
90
|
+
file: filePath,
|
|
91
|
+
bytes: Buffer.byteLength(content, 'utf8'),
|
|
92
|
+
message: `Project business context (${path.relative(process.cwd(), filePath)}) synced to cloud successfully.`,
|
|
93
|
+
updated_at: syncResult.result.updated_at || new Date().toISOString(),
|
|
94
|
+
});
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
case 'pull': {
|
|
99
|
+
if (fs.existsSync(filePath) && !flags.force) {
|
|
100
|
+
const displayPath = path.relative(process.cwd(), filePath) || filePath;
|
|
101
|
+
const err = new Error(`Local file ${displayPath} already exists. Pass --force to overwrite.`);
|
|
102
|
+
err.code = 'FILE_EXISTS';
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const pullResult = await executeWithSafeFallback(async () => {
|
|
107
|
+
return ctx.client.getProjectContext(appId);
|
|
108
|
+
}, 2, 500);
|
|
109
|
+
|
|
110
|
+
if (!pullResult.success || !pullResult.result || !pullResult.result.context_markdown) {
|
|
111
|
+
ctx.output({
|
|
112
|
+
ok: true,
|
|
113
|
+
skipped: true,
|
|
114
|
+
action: 'pull',
|
|
115
|
+
app_id: appId,
|
|
116
|
+
message: 'Server connection timed out (2 attempts). Skipped cloud pull without blocking.',
|
|
117
|
+
});
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
122
|
+
fs.writeFileSync(filePath, pullResult.result.context_markdown, 'utf8');
|
|
123
|
+
|
|
124
|
+
ctx.output({
|
|
125
|
+
ok: true,
|
|
126
|
+
action: 'pull',
|
|
127
|
+
app_id: appId,
|
|
128
|
+
file: filePath,
|
|
129
|
+
bytes: Buffer.byteLength(pullResult.result.context_markdown, 'utf8'),
|
|
130
|
+
message: `Business context pulled and saved to ${path.relative(process.cwd(), filePath)}.`,
|
|
131
|
+
updated_at: pullResult.result.updated_at,
|
|
132
|
+
});
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
case 'show': {
|
|
137
|
+
let content = null;
|
|
138
|
+
let source = 'local';
|
|
139
|
+
|
|
140
|
+
if (!flags.cloud && fs.existsSync(filePath)) {
|
|
141
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
142
|
+
source = 'local';
|
|
143
|
+
} else {
|
|
144
|
+
const fetchResult = await executeWithSafeFallback(async () => {
|
|
145
|
+
return ctx.client.getProjectContext(appId);
|
|
146
|
+
}, 2, 500);
|
|
147
|
+
|
|
148
|
+
if (fetchResult.success && fetchResult.result && fetchResult.result.context_markdown) {
|
|
149
|
+
content = fetchResult.result.context_markdown;
|
|
150
|
+
source = 'cloud';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!content) {
|
|
155
|
+
const displayPath = path.relative(process.cwd(), filePath) || filePath;
|
|
156
|
+
ctx.output({
|
|
157
|
+
ok: true,
|
|
158
|
+
app_id: appId,
|
|
159
|
+
source: 'none',
|
|
160
|
+
message: `No context found locally (${displayPath}) or in cloud. Server lookup skipped after timeout.`,
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
ctx.output({
|
|
166
|
+
ok: true,
|
|
167
|
+
app_id: appId,
|
|
168
|
+
source,
|
|
169
|
+
file: source === 'local' ? filePath : null,
|
|
170
|
+
context_markdown: content,
|
|
171
|
+
});
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
default:
|
|
176
|
+
throw new Error(`Unknown context subcommand: ${targetSub}. Supported: push, pull, show`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = {
|
|
181
|
+
getContextFilePath,
|
|
182
|
+
handleContext,
|
|
183
|
+
extractAppIdFromMarkdown,
|
|
184
|
+
executeWithSafeFallback,
|
|
185
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
async function handleDashboards(subcommand, args, flags, ctx) {
|
|
4
|
+
switch (subcommand) {
|
|
5
|
+
case 'list': {
|
|
6
|
+
const appId = ctx.resolveAppId();
|
|
7
|
+
const list = await ctx.client.listDashboards(appId);
|
|
8
|
+
ctx.output({
|
|
9
|
+
ok: true,
|
|
10
|
+
dashboards: list,
|
|
11
|
+
}, () => {
|
|
12
|
+
if (!list || list.length === 0) {
|
|
13
|
+
console.log('No custom dashboards found. Create one using shiplens dashboards create.');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
console.log(`📊 Found ${list.length} dashboard(s):\n`);
|
|
17
|
+
list.forEach((d, i) => {
|
|
18
|
+
const defTag = d.is_default ? ' [Default]' : '';
|
|
19
|
+
console.log(`[${i + 1}] ${d.title} (ID: ${d.dashboard_id})${defTag}`);
|
|
20
|
+
if (d.url) console.log(` URL: ${d.url}`);
|
|
21
|
+
console.log('');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
case 'create': {
|
|
28
|
+
const useAI = Boolean(flags.ai);
|
|
29
|
+
const title = flags.title || 'AI Telemetry Dashboard';
|
|
30
|
+
const prompt = flags.prompt || '';
|
|
31
|
+
|
|
32
|
+
if (useAI) {
|
|
33
|
+
let appId = '';
|
|
34
|
+
try { appId = ctx.resolveAppId(); } catch (_) {}
|
|
35
|
+
|
|
36
|
+
if (!prompt) {
|
|
37
|
+
throw new Error('Provide natural language dashboard prompt via --prompt when using --ai mode.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const reqData = { title, prompt };
|
|
41
|
+
if (appId) reqData.app_id = appId;
|
|
42
|
+
|
|
43
|
+
const resp = await ctx.client.createAIDashboard(reqData);
|
|
44
|
+
ctx.output(resp, () => {
|
|
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}`);
|
|
48
|
+
});
|
|
49
|
+
} else {
|
|
50
|
+
const appId = ctx.resolveAppId();
|
|
51
|
+
const reqData = { title, prompt, app_id: appId };
|
|
52
|
+
|
|
53
|
+
if (flags.file) {
|
|
54
|
+
if (!fs.existsSync(flags.file)) {
|
|
55
|
+
throw new Error(`Dashboard config file not found: ${flags.file}`);
|
|
56
|
+
}
|
|
57
|
+
const fileContent = JSON.parse(fs.readFileSync(flags.file, 'utf8'));
|
|
58
|
+
Object.assign(reqData, fileContent);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!reqData.prompt && (!reqData.widgets || reqData.widgets.length === 0)) {
|
|
62
|
+
throw new Error('Provide generation prompt via --prompt or configuration via --file.');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const resp = await ctx.client.createDashboard(appId, reqData);
|
|
66
|
+
ctx.output(resp, () => {
|
|
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}`);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
default:
|
|
76
|
+
throw new Error(`Unknown dashboards subcommand: ${subcommand}. Supported: list, create`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { handleDashboards };
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { getLocalConfig } = require('../config');
|
|
4
|
+
const { detectProject } = require('../injector');
|
|
5
|
+
|
|
6
|
+
async function handleDoctor(args, flags, ctx) {
|
|
7
|
+
const wd = process.cwd();
|
|
8
|
+
const checks = {};
|
|
9
|
+
let hasFail = false;
|
|
10
|
+
let hasWarn = false;
|
|
11
|
+
|
|
12
|
+
// 1. local_config check
|
|
13
|
+
const localCfg = getLocalConfig(wd);
|
|
14
|
+
if (localCfg && localCfg.app_id) {
|
|
15
|
+
checks.local_config = {
|
|
16
|
+
status: 'pass',
|
|
17
|
+
app_id: localCfg.app_id,
|
|
18
|
+
};
|
|
19
|
+
} else {
|
|
20
|
+
hasWarn = true;
|
|
21
|
+
checks.local_config = {
|
|
22
|
+
status: 'fail',
|
|
23
|
+
message: 'No .shiplens.json or missing app_id in current directory',
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 2. sdk_dependency check
|
|
28
|
+
const sdkCheck = { status: 'pass' };
|
|
29
|
+
const pkgPath = path.join(wd, 'package.json');
|
|
30
|
+
if (fs.existsSync(pkgPath)) {
|
|
31
|
+
try {
|
|
32
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
33
|
+
const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
|
|
34
|
+
if (deps['@shiplens/sdk']) {
|
|
35
|
+
sdkCheck.version = deps['@shiplens/sdk'];
|
|
36
|
+
} else {
|
|
37
|
+
sdkCheck.status = 'warn';
|
|
38
|
+
sdkCheck.message = '@shiplens/sdk not explicitly found in package.json';
|
|
39
|
+
hasWarn = true;
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {
|
|
42
|
+
sdkCheck.status = 'warn';
|
|
43
|
+
sdkCheck.message = 'Unable to parse package.json';
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
sdkCheck.status = 'warn';
|
|
47
|
+
sdkCheck.message = 'package.json not found (static HTML or non-Node project)';
|
|
48
|
+
}
|
|
49
|
+
checks.sdk_dependency = sdkCheck;
|
|
50
|
+
|
|
51
|
+
// 3. code_injection check
|
|
52
|
+
const detected = detectProject(wd);
|
|
53
|
+
let injectedFile = '';
|
|
54
|
+
const candidates = [
|
|
55
|
+
'src/app/layout.tsx', 'app/layout.tsx', 'src/app/layout.js', 'app/layout.js',
|
|
56
|
+
'src/pages/_app.tsx', 'pages/_app.tsx', 'src/pages/_app.js', 'pages/_app.js',
|
|
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',
|
|
59
|
+
'src/App.vue', 'index.html', 'public/index.html', 'shiplens.config.ts',
|
|
60
|
+
'src/components/ShiplensTracker.tsx', 'components/ShiplensTracker.tsx',
|
|
61
|
+
];
|
|
62
|
+
for (const cand of candidates) {
|
|
63
|
+
const p = path.join(wd, cand);
|
|
64
|
+
if (fs.existsSync(p)) {
|
|
65
|
+
const content = fs.readFileSync(p, 'utf8');
|
|
66
|
+
if (content.includes('initShiplens') || content.includes('cdn.shiplens.dev/sdk.js')) {
|
|
67
|
+
injectedFile = cand;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (injectedFile) {
|
|
74
|
+
checks.code_injection = {
|
|
75
|
+
status: 'pass',
|
|
76
|
+
file: injectedFile,
|
|
77
|
+
};
|
|
78
|
+
} else {
|
|
79
|
+
hasWarn = true;
|
|
80
|
+
checks.code_injection = {
|
|
81
|
+
status: 'warn',
|
|
82
|
+
message: `No initShiplens call detected in entry files (Framework: ${detected.framework})`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 4. ingestion_connectivity check
|
|
87
|
+
try {
|
|
88
|
+
const start = Date.now();
|
|
89
|
+
await ctx.client.me();
|
|
90
|
+
checks.ingestion_connectivity = {
|
|
91
|
+
status: 'pass',
|
|
92
|
+
latency_ms: Date.now() - start,
|
|
93
|
+
};
|
|
94
|
+
} catch (err) {
|
|
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
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 5. auth_credential check
|
|
110
|
+
if (ctx.resolvedAuth.is_present) {
|
|
111
|
+
checks.auth_credential = {
|
|
112
|
+
status: 'pass',
|
|
113
|
+
source: ctx.resolvedAuth.source,
|
|
114
|
+
};
|
|
115
|
+
} else {
|
|
116
|
+
checks.auth_credential = {
|
|
117
|
+
status: 'warn',
|
|
118
|
+
message: 'No Access Secret configured (login required for advanced features)',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 6. email_binding check
|
|
123
|
+
if (localCfg && localCfg.bound_email) {
|
|
124
|
+
checks.email_binding = {
|
|
125
|
+
status: 'pass',
|
|
126
|
+
email: localCfg.bound_email,
|
|
127
|
+
};
|
|
128
|
+
} else {
|
|
129
|
+
hasWarn = true;
|
|
130
|
+
checks.email_binding = {
|
|
131
|
+
status: 'warn',
|
|
132
|
+
message: 'Email not bound. Run: shiplens auth bind --email <you@example.com>',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 7. schema_freshness check
|
|
137
|
+
if (localCfg && localCfg.schema_last_modified_at) {
|
|
138
|
+
const ageMs = Date.now() - localCfg.schema_last_modified_at * 1000;
|
|
139
|
+
const ageDays = ageMs / (1000 * 60 * 60 * 24);
|
|
140
|
+
if (ageDays > 7) {
|
|
141
|
+
hasWarn = true;
|
|
142
|
+
checks.schema_freshness = {
|
|
143
|
+
status: 'warn',
|
|
144
|
+
age_days: Math.floor(ageDays),
|
|
145
|
+
message: `Local schema is ${Math.floor(ageDays)} days old. Run shiplens projects list to refresh.`,
|
|
146
|
+
};
|
|
147
|
+
} else {
|
|
148
|
+
checks.schema_freshness = {
|
|
149
|
+
status: 'pass',
|
|
150
|
+
age_days: Math.floor(ageDays),
|
|
151
|
+
schema_last_modified_at: localCfg.schema_last_modified_at,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
checks.schema_freshness = {
|
|
156
|
+
status: 'warn',
|
|
157
|
+
message: 'No schema_last_modified_at recorded locally. Run shiplens init or projects list.',
|
|
158
|
+
};
|
|
159
|
+
hasWarn = true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let overall = 'healthy';
|
|
163
|
+
if (hasFail) {
|
|
164
|
+
overall = 'unhealthy';
|
|
165
|
+
} else if (hasWarn) {
|
|
166
|
+
overall = 'warning';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const report = {
|
|
170
|
+
ok: !hasFail,
|
|
171
|
+
checks,
|
|
172
|
+
overall,
|
|
173
|
+
doctor_ran_at: new Date().toISOString(),
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
ctx.output(report, () => {
|
|
177
|
+
console.log('🏥 Shiplens Environment & Telemetry Diagnostic Report');
|
|
178
|
+
console.log('='.repeat(50));
|
|
179
|
+
for (const [name, item] of Object.entries(checks)) {
|
|
180
|
+
let icon = '✅';
|
|
181
|
+
if (item.status === 'warn') icon = '⚠️';
|
|
182
|
+
else if (item.status === 'fail') icon = '❌';
|
|
183
|
+
|
|
184
|
+
console.log(`${icon} [${name}]: ${item.status}`);
|
|
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}`);
|
|
193
|
+
}
|
|
194
|
+
console.log('-'.repeat(50));
|
|
195
|
+
console.log(`🩺 Overall Status: ${overall}`);
|
|
196
|
+
console.log(`⏱ Timestamp: ${report.doctor_ran_at}`);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = { handleDoctor };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
async function handleHeatmap(args, flags, ctx) {
|
|
2
|
+
const appId = ctx.resolveAppId();
|
|
3
|
+
const templateId = flags.template || args[0];
|
|
4
|
+
if (!templateId) {
|
|
5
|
+
throw new Error('Specify target page template ID via --template <id>');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const env = flags.env || 'production';
|
|
9
|
+
const domHash = flags['dom-hash'] || '';
|
|
10
|
+
|
|
11
|
+
const resp = await ctx.client.getHeatmap({
|
|
12
|
+
app_id: appId,
|
|
13
|
+
template_id: templateId,
|
|
14
|
+
env,
|
|
15
|
+
dom_hash: domHash,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
ctx.output(resp, () => {
|
|
19
|
+
console.log(`🔥 Page Heatmap Analysis (Template: ${resp.template_id}, DOM Hash: ${resp.dom_hash || '-'}):`);
|
|
20
|
+
if (resp.skeleton_svg_url) {
|
|
21
|
+
console.log(`🖼️ Skeleton SVG Preview URL: ${resp.skeleton_svg_url}`);
|
|
22
|
+
}
|
|
23
|
+
console.log(`👆 Total Clicks: ${resp.clicks_total || 0}\n`);
|
|
24
|
+
if (resp.elements && resp.elements.length > 0) {
|
|
25
|
+
console.log('Top Clicked Elements:');
|
|
26
|
+
resp.elements.forEach((el, i) => {
|
|
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})`);
|
|
29
|
+
});
|
|
30
|
+
} else {
|
|
31
|
+
console.log('(No click data found for this template)');
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { handleHeatmap };
|