@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/auth.js
CHANGED
|
@@ -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(`✅
|
|
15
|
-
console.log('
|
|
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: '
|
|
22
|
+
message: 'No credentials detected. Configure via shiplens auth set.',
|
|
23
23
|
}, () => {
|
|
24
|
-
console.log('⚠️
|
|
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(`👤
|
|
41
|
-
console.log(`🔑
|
|
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(`❌
|
|
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('
|
|
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(
|
|
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('
|
|
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(
|
|
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 ? `✅
|
|
79
|
-
console.log('
|
|
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('
|
|
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: '
|
|
126
|
+
message: 'Access Secret saved securely to ~/.shiplens/config.json',
|
|
127
127
|
}, () => {
|
|
128
|
-
console.log(`✅
|
|
129
|
-
if (email) console.log(`👤
|
|
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('
|
|
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(`✅
|
|
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('
|
|
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(`✅
|
|
153
|
+
return ctx.output({ ok: true, ...response }, () => console.log(`✅ Revoked Access Secret: ${response.api_key_id}`));
|
|
154
154
|
}
|
|
155
|
-
throw new Error('
|
|
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(`👤
|
|
162
|
-
console.log(`📧
|
|
161
|
+
console.log(`👤 User ID: ${meResp.user_id}`);
|
|
162
|
+
console.log(`📧 Email: ${meResp.email}`);
|
|
163
163
|
if (meResp.project_count) {
|
|
164
|
-
console.log(`📦
|
|
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('
|
|
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(`❌
|
|
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:
|
|
228
|
+
message: `Activation email sent to ${email}. Click the link to complete registration.`,
|
|
234
229
|
}, () => {
|
|
235
|
-
console.log(`📧
|
|
236
|
-
if (appId) console.log(`📦
|
|
237
|
-
console.log('👉
|
|
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(
|
|
238
|
+
throw new Error(`Unknown auth subcommand: ${subcommand}. Supported: status, set, secret, whoami, logout, bind, mcp-config, configure`);
|
|
244
239
|
}
|
|
245
240
|
}
|
|
246
241
|
|
package/lib/commands/context.js
CHANGED
|
@@ -3,7 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const { getLocalConfig } = require('../config');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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(
|
|
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(
|
|
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: '
|
|
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:
|
|
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(
|
|
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: '
|
|
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:
|
|
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:
|
|
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(
|
|
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('
|
|
13
|
+
console.log('No custom dashboards found. Create one using shiplens dashboards create.');
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
|
-
console.log(`📊
|
|
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(`
|
|
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('
|
|
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(`📌
|
|
54
|
-
if (resp.dashboard_url) console.log(`🔗
|
|
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(
|
|
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('
|
|
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(`📌
|
|
77
|
-
if (resp.dashboard_url) console.log(`🔗
|
|
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(
|
|
76
|
+
throw new Error(`Unknown dashboards subcommand: ${subcommand}. Supported: list, create`);
|
|
85
77
|
}
|
|
86
78
|
}
|
|
87
79
|
|
package/lib/commands/doctor.js
CHANGED
|
@@ -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: '
|
|
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 = '
|
|
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 = '
|
|
43
|
+
sdkCheck.message = 'Unable to parse package.json';
|
|
44
44
|
}
|
|
45
45
|
} else {
|
|
46
46
|
sdkCheck.status = 'warn';
|
|
47
|
-
sdkCheck.message = '
|
|
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:
|
|
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
|
|
88
|
+
const start = Date.now();
|
|
89
|
+
await ctx.client.me();
|
|
88
90
|
checks.ingestion_connectivity = {
|
|
89
91
|
status: 'pass',
|
|
90
|
-
latency_ms:
|
|
92
|
+
latency_ms: Date.now() - start,
|
|
91
93
|
};
|
|
92
94
|
} catch (err) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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: '
|
|
118
|
+
message: 'No Access Secret configured (login required for advanced features)',
|
|
110
119
|
};
|
|
111
120
|
}
|
|
112
121
|
|
|
113
|
-
// 6. email_binding
|
|
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: '
|
|
132
|
+
message: 'Email not bound. Run: shiplens auth bind --email <you@example.com>',
|
|
124
133
|
};
|
|
125
134
|
}
|
|
126
135
|
|
|
127
|
-
// 7. schema_freshness
|
|
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:
|
|
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: '
|
|
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(`
|
|
177
|
-
if (item.email) console.log(`
|
|
178
|
-
if (item.version) console.log(`
|
|
179
|
-
if (item.file) console.log(`
|
|
180
|
-
if (item.latency_ms !== undefined) console.log(`
|
|
181
|
-
if (item.source) console.log(`
|
|
182
|
-
if (item.age_days !== undefined) console.log(` Schema
|
|
183
|
-
if (item.message) console.log(`
|
|
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(`🩺
|
|
187
|
-
console.log(`⏱
|
|
195
|
+
console.log(`🩺 Overall Status: ${overall}`);
|
|
196
|
+
console.log(`⏱ Timestamp: ${report.doctor_ran_at}`);
|
|
188
197
|
});
|
|
189
198
|
}
|
|
190
199
|
|