@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
package/lib/auth.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { getGlobalConfig, saveGlobalConfig } = require('./config');
|
|
4
|
+
|
|
5
|
+
function maskSecret(secret) {
|
|
6
|
+
if (!secret || secret.length <= 8) {
|
|
7
|
+
return '******';
|
|
8
|
+
}
|
|
9
|
+
const prefixLen = secret.length < 12 ? 3 : 7;
|
|
10
|
+
const suffixLen = 4;
|
|
11
|
+
if (secret.length < prefixLen + suffixLen) {
|
|
12
|
+
return secret.slice(0, 3) + '...' + secret.slice(-2);
|
|
13
|
+
}
|
|
14
|
+
return secret.slice(0, prefixLen) + '...' + secret.slice(-suffixLen);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readDotEnvSecret(dir = process.cwd()) {
|
|
18
|
+
const envFiles = [path.join(dir, '.shiplens.env'), path.join(dir, '.env')];
|
|
19
|
+
for (const f of envFiles) {
|
|
20
|
+
if (fs.existsSync(f)) {
|
|
21
|
+
try {
|
|
22
|
+
const content = fs.readFileSync(f, 'utf8');
|
|
23
|
+
const lines = content.split('\n');
|
|
24
|
+
for (const line of lines) {
|
|
25
|
+
const trimmed = line.trim();
|
|
26
|
+
if (trimmed.startsWith('#') || !trimmed.includes('=')) continue;
|
|
27
|
+
const [k, ...v] = trimmed.split('=');
|
|
28
|
+
const key = k.trim();
|
|
29
|
+
const val = v.join('=').trim().replace(/^["']|["']$/g, '');
|
|
30
|
+
if (key === 'SHIPLENS_ACCESS_SECRET' || key === 'SHIPLENS_MCP_TOKEN') {
|
|
31
|
+
return val;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} catch (e) {}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveSecret(explicitSecret) {
|
|
41
|
+
if (explicitSecret) {
|
|
42
|
+
return {
|
|
43
|
+
secret: explicitSecret,
|
|
44
|
+
masked_secret: maskSecret(explicitSecret),
|
|
45
|
+
source: 'flag',
|
|
46
|
+
is_present: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const envSec = process.env.SHIPLENS_ACCESS_SECRET || process.env.SHIPLENS_MCP_TOKEN;
|
|
51
|
+
if (envSec) {
|
|
52
|
+
return {
|
|
53
|
+
secret: envSec,
|
|
54
|
+
masked_secret: maskSecret(envSec),
|
|
55
|
+
source: 'environment',
|
|
56
|
+
is_present: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const dotEnvSec = readDotEnvSecret();
|
|
61
|
+
if (dotEnvSec) {
|
|
62
|
+
return {
|
|
63
|
+
secret: dotEnvSec,
|
|
64
|
+
masked_secret: maskSecret(dotEnvSec),
|
|
65
|
+
source: 'dotenv',
|
|
66
|
+
is_present: true,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const gCfg = getGlobalConfig();
|
|
71
|
+
if (gCfg && gCfg.secret) {
|
|
72
|
+
return {
|
|
73
|
+
secret: gCfg.secret,
|
|
74
|
+
masked_secret: maskSecret(gCfg.secret),
|
|
75
|
+
source: 'config',
|
|
76
|
+
is_present: true,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
secret: '',
|
|
82
|
+
masked_secret: '',
|
|
83
|
+
source: 'none',
|
|
84
|
+
is_present: false,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function saveSecretToGlobal(secret, email = '', userId = '') {
|
|
89
|
+
const gCfg = getGlobalConfig();
|
|
90
|
+
gCfg.secret = secret;
|
|
91
|
+
if (email) gCfg.email = email;
|
|
92
|
+
if (userId) gCfg.user_id = userId;
|
|
93
|
+
saveGlobalConfig(gCfg);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function clearGlobalSecret() {
|
|
97
|
+
const gCfg = getGlobalConfig();
|
|
98
|
+
gCfg.secret = '';
|
|
99
|
+
saveGlobalConfig(gCfg);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = {
|
|
103
|
+
maskSecret,
|
|
104
|
+
resolveSecret,
|
|
105
|
+
saveSecretToGlobal,
|
|
106
|
+
clearGlobalSecret,
|
|
107
|
+
};
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
const { APIClient } = require('./api');
|
|
2
|
+
const { resolveSecret } = require('./auth');
|
|
3
|
+
const { getLocalConfig } = require('./config');
|
|
4
|
+
|
|
5
|
+
const { handleInit } = require('./commands/init');
|
|
6
|
+
const { handleAuth } = require('./commands/auth');
|
|
7
|
+
const { handleProjects } = require('./commands/projects');
|
|
8
|
+
const { handleQuery } = require('./commands/query');
|
|
9
|
+
const { handleSQL } = require('./commands/sql');
|
|
10
|
+
const { handleSummary } = require('./commands/summary');
|
|
11
|
+
const { handlePages, handlePaths, handleCanvas } = require('./commands/pages');
|
|
12
|
+
const { handleHeatmap } = require('./commands/heatmap');
|
|
13
|
+
const { handleDashboards } = require('./commands/dashboards');
|
|
14
|
+
const { handleDoctor } = require('./commands/doctor');
|
|
15
|
+
const { handleContext } = require('./commands/context');
|
|
16
|
+
const { handleMcp } = require('./commands/mcp');
|
|
17
|
+
|
|
18
|
+
const VERSION = '1.2.7';
|
|
19
|
+
|
|
20
|
+
function parseArgs(argv) {
|
|
21
|
+
const flags = {};
|
|
22
|
+
const args = [];
|
|
23
|
+
let i = 0;
|
|
24
|
+
|
|
25
|
+
while (i < argv.length) {
|
|
26
|
+
const arg = argv[i];
|
|
27
|
+
if (arg.startsWith('--')) {
|
|
28
|
+
const equalIndex = arg.indexOf('=');
|
|
29
|
+
if (equalIndex !== -1) {
|
|
30
|
+
const key = arg.slice(2, equalIndex);
|
|
31
|
+
const val = arg.slice(equalIndex + 1);
|
|
32
|
+
if (flags[key]) {
|
|
33
|
+
if (Array.isArray(flags[key])) flags[key].push(val);
|
|
34
|
+
else flags[key] = [flags[key], val];
|
|
35
|
+
} else {
|
|
36
|
+
flags[key] = val;
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
const key = arg.slice(2);
|
|
40
|
+
const next = argv[i + 1];
|
|
41
|
+
if (next && !next.startsWith('-')) {
|
|
42
|
+
if (flags[key]) {
|
|
43
|
+
if (Array.isArray(flags[key])) flags[key].push(next);
|
|
44
|
+
else flags[key] = [flags[key], next];
|
|
45
|
+
} else {
|
|
46
|
+
flags[key] = next;
|
|
47
|
+
}
|
|
48
|
+
i++;
|
|
49
|
+
} else {
|
|
50
|
+
flags[key] = true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} else if (arg.startsWith('-')) {
|
|
54
|
+
const key = arg.slice(1);
|
|
55
|
+
if (key === 'v') flags.version = true;
|
|
56
|
+
else if (key === 'h') flags.help = true;
|
|
57
|
+
else flags[key] = true;
|
|
58
|
+
} else {
|
|
59
|
+
args.push(arg);
|
|
60
|
+
}
|
|
61
|
+
i++;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { args, flags };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function printHelp() {
|
|
68
|
+
console.log(`Shiplens CLI - High-Speed User Telemetry & SDK Management Tool (v${VERSION})
|
|
69
|
+
|
|
70
|
+
Usage:
|
|
71
|
+
shiplens <command> [subcommand] [options]
|
|
72
|
+
npx.cmd --yes shiplens-cli <command> [subcommand] [options] (Windows)
|
|
73
|
+
npx --yes shiplens-cli <command> [subcommand] [options] (macOS/Linux)
|
|
74
|
+
|
|
75
|
+
Core Commands:
|
|
76
|
+
init 15-second zero-config analytics initialization & dashboard setup
|
|
77
|
+
auth Authentication & credentials (status, set, secret, whoami, logout, bind, mcp-config, configure)
|
|
78
|
+
projects Project management (list, bind, delete)
|
|
79
|
+
query Multi-dimensional metrics and conversion funnel queries
|
|
80
|
+
sql Execute secure read-only SQL queries on ClickHouse
|
|
81
|
+
summary Retrieve product overview (traffic, bounce rates, geos, devices)
|
|
82
|
+
pages Inspect page-level visits and average dwell times
|
|
83
|
+
paths Analyze user flow journeys and Sankey transition paths
|
|
84
|
+
canvas Retrieve global user behavior canvas topology
|
|
85
|
+
heatmap Retrieve click heatmaps and skeleton wireframes
|
|
86
|
+
dashboards AI dashboard management and one-click creation (list, create [--ai])
|
|
87
|
+
doctor Run full end-to-end diagnostics on SDK, credentials, and network
|
|
88
|
+
context Manage business context dictionary (push, pull, show)
|
|
89
|
+
mcp serve Run local MCP proxy server with device credentials
|
|
90
|
+
|
|
91
|
+
auth Subcommands:
|
|
92
|
+
auth status Check credential validity
|
|
93
|
+
auth set [--secret-stdin] Save Access Secret
|
|
94
|
+
auth secret list List Access Secrets (masked)
|
|
95
|
+
auth secret create [--scopes ...] Create scoped offline Access Secret
|
|
96
|
+
auth secret revoke --key-id <id> Revoke offline Access Secret
|
|
97
|
+
auth whoami Display current authenticated user
|
|
98
|
+
auth logout Clear local credentials
|
|
99
|
+
auth bind --email <email> [--json] Send Magic Link email for account binding
|
|
100
|
+
auth mcp-config [--client <client>] Print MCP configuration (cursor/codex/claude/antigravity/manual)
|
|
101
|
+
auth configure --client <client> Write MCP configuration into target client
|
|
102
|
+
auth configure --client manual Output standard MCP JSON configuration
|
|
103
|
+
|
|
104
|
+
init Dedicated Options:
|
|
105
|
+
--name <string> Project name (default: auto-read from package.json)
|
|
106
|
+
--description <string> Project functional description & positioning
|
|
107
|
+
--genre <id> Level 1 category ID (e.g. utilities, finance, casual_games)
|
|
108
|
+
--subgenre <id> Level 2 subcategory ID (e.g. developer_tools, financial_tools)
|
|
109
|
+
--tags <ids> Level 4 feature tags (comma-separated tag_ids)
|
|
110
|
+
--industry <string> Compatibility industry category field
|
|
111
|
+
--email <email|auto> Auto or manual binding email
|
|
112
|
+
|
|
113
|
+
Global Options:
|
|
114
|
+
--json Output in standard JSON format (required for AI Agents)
|
|
115
|
+
--force Force overwrite existing local configuration and project ID
|
|
116
|
+
--no-commit Skip automatic Git commit after initialization
|
|
117
|
+
--app-id <id> Explicitly specify target project app_id
|
|
118
|
+
--env <env> Target environment (production / staging, default: production)
|
|
119
|
+
--secret <key> Pass Access Secret explicitly
|
|
120
|
+
--api-url <url> Custom backend API base URL (default: http://120.26.230.33)
|
|
121
|
+
-v, --version Display CLI version
|
|
122
|
+
-h, --help Display help information
|
|
123
|
+
`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function runCLI(argv = process.argv.slice(2)) {
|
|
127
|
+
const { args, flags } = parseArgs(argv);
|
|
128
|
+
|
|
129
|
+
if (flags.version || (args.length === 0 && flags.v)) {
|
|
130
|
+
if (flags.json) {
|
|
131
|
+
console.log(JSON.stringify({ ok: true, version: VERSION }));
|
|
132
|
+
} else {
|
|
133
|
+
console.log(`shiplens v${VERSION}`);
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (flags.help || args.length === 0 || args[0] === 'help') {
|
|
139
|
+
printHelp();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const isJSON = Boolean(flags.json);
|
|
144
|
+
|
|
145
|
+
const output = (data, textFormatter) => {
|
|
146
|
+
if (isJSON) {
|
|
147
|
+
console.log(JSON.stringify(data, null, 2));
|
|
148
|
+
} else if (textFormatter) {
|
|
149
|
+
textFormatter();
|
|
150
|
+
} else {
|
|
151
|
+
console.log(JSON.stringify(data, null, 2));
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const outputError = (err) => {
|
|
156
|
+
const code = err.code || 'INTERNAL_ERROR';
|
|
157
|
+
if (isJSON) {
|
|
158
|
+
console.log(JSON.stringify({
|
|
159
|
+
ok: false,
|
|
160
|
+
code,
|
|
161
|
+
message: err.message,
|
|
162
|
+
status: err.status,
|
|
163
|
+
}, null, 2));
|
|
164
|
+
} else {
|
|
165
|
+
console.error(`❌ Error [${code}]: ${err.message}`);
|
|
166
|
+
}
|
|
167
|
+
process.exitCode = 1;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Resolve credentials and API client
|
|
171
|
+
const resolvedAuth = resolveSecret(flags.secret);
|
|
172
|
+
let apiUrl = flags['api-url'] || process.env.SHIPLENS_API_URL;
|
|
173
|
+
if (!apiUrl) {
|
|
174
|
+
const localCfg = getLocalConfig();
|
|
175
|
+
if (localCfg && localCfg.api_url) apiUrl = localCfg.api_url;
|
|
176
|
+
}
|
|
177
|
+
if (!apiUrl) apiUrl = 'http://120.26.230.33';
|
|
178
|
+
|
|
179
|
+
const client = new APIClient(apiUrl, resolvedAuth.secret);
|
|
180
|
+
|
|
181
|
+
const resolveAppId = () => {
|
|
182
|
+
if (flags['app-id']) return flags['app-id'];
|
|
183
|
+
const localCfg = getLocalConfig();
|
|
184
|
+
if (localCfg && localCfg.app_id) return localCfg.app_id;
|
|
185
|
+
const err = new Error('Target app_id not found. Provide --app-id or run inside an initialized project directory.');
|
|
186
|
+
err.code = 'APP_NOT_FOUND';
|
|
187
|
+
throw err;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const ctx = {
|
|
191
|
+
client,
|
|
192
|
+
resolvedAuth,
|
|
193
|
+
resolveAppId,
|
|
194
|
+
output,
|
|
195
|
+
isJSON,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const command = args[0];
|
|
199
|
+
const subcommand = args[1];
|
|
200
|
+
const cmdArgs = args.slice(1);
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
switch (command) {
|
|
204
|
+
case 'init':
|
|
205
|
+
await handleInit(cmdArgs, flags, ctx);
|
|
206
|
+
break;
|
|
207
|
+
case 'auth':
|
|
208
|
+
await handleAuth(subcommand || 'status', args.slice(2), flags, ctx);
|
|
209
|
+
break;
|
|
210
|
+
case 'projects':
|
|
211
|
+
await handleProjects(subcommand || 'list', args.slice(2), flags, ctx);
|
|
212
|
+
break;
|
|
213
|
+
case 'query':
|
|
214
|
+
await handleQuery(cmdArgs, flags, ctx);
|
|
215
|
+
break;
|
|
216
|
+
case 'sql':
|
|
217
|
+
await handleSQL(cmdArgs, flags, ctx);
|
|
218
|
+
break;
|
|
219
|
+
case 'summary':
|
|
220
|
+
await handleSummary(cmdArgs, flags, ctx);
|
|
221
|
+
break;
|
|
222
|
+
case 'pages':
|
|
223
|
+
await handlePages(cmdArgs, flags, ctx);
|
|
224
|
+
break;
|
|
225
|
+
case 'paths':
|
|
226
|
+
await handlePaths(cmdArgs, flags, ctx);
|
|
227
|
+
break;
|
|
228
|
+
case 'canvas':
|
|
229
|
+
await handleCanvas(cmdArgs, flags, ctx);
|
|
230
|
+
break;
|
|
231
|
+
case 'heatmap':
|
|
232
|
+
await handleHeatmap(cmdArgs, flags, ctx);
|
|
233
|
+
break;
|
|
234
|
+
case 'dashboards':
|
|
235
|
+
await handleDashboards(subcommand || 'list', args.slice(2), flags, ctx);
|
|
236
|
+
break;
|
|
237
|
+
case 'doctor':
|
|
238
|
+
await handleDoctor(cmdArgs, flags, ctx);
|
|
239
|
+
break;
|
|
240
|
+
case 'context':
|
|
241
|
+
await handleContext(subcommand || 'show', args.slice(2), flags, ctx);
|
|
242
|
+
break;
|
|
243
|
+
case 'mcp':
|
|
244
|
+
await handleMcp(subcommand || 'serve');
|
|
245
|
+
break;
|
|
246
|
+
default:
|
|
247
|
+
throw new Error(`Unknown command: ${command}. Use shiplens --help to view supported commands.`);
|
|
248
|
+
}
|
|
249
|
+
} catch (err) {
|
|
250
|
+
outputError(err);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
module.exports = {
|
|
255
|
+
runCLI,
|
|
256
|
+
parseArgs,
|
|
257
|
+
VERSION,
|
|
258
|
+
};
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
const readline = require('readline');
|
|
2
|
+
const { getDeviceEnv } = require('../config');
|
|
3
|
+
const { saveDeviceEnv } = require('../device-env');
|
|
4
|
+
const { CLIENTS, getMcpConfig, configureMcpClient } = require('../mcp-config');
|
|
5
|
+
const { saveSecretToGlobal, clearGlobalSecret, maskSecret } = require('../auth');
|
|
6
|
+
const { APIClient } = require('../api');
|
|
7
|
+
|
|
8
|
+
async function handleAuth(subcommand, args, flags, ctx) {
|
|
9
|
+
switch (subcommand) {
|
|
10
|
+
case 'status': {
|
|
11
|
+
const deviceEnv = getDeviceEnv();
|
|
12
|
+
if (deviceEnv) {
|
|
13
|
+
return ctx.output({ ok: true, authenticated: false, device_env: 'present', mcp_url: deviceEnv.SHIPLENS_MCP_URL, next: 'Use shiplens auth mcp-config, then complete OAuth in your Agent.' }, () => {
|
|
14
|
+
console.log(`✅ shiplens.env detected; MCP: ${deviceEnv.SHIPLENS_MCP_URL}`);
|
|
15
|
+
console.log('Complete OAuth login in your Agent before using MCP.');
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
if (!ctx.resolvedAuth.is_present) {
|
|
19
|
+
return ctx.output({
|
|
20
|
+
ok: false,
|
|
21
|
+
authenticated: false,
|
|
22
|
+
message: 'No credentials detected. Configure via shiplens auth set.',
|
|
23
|
+
}, () => {
|
|
24
|
+
console.log('⚠️ No valid credentials configured. Run: shiplens auth set');
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const meResp = await ctx.client.me();
|
|
30
|
+
ctx.output({
|
|
31
|
+
ok: true,
|
|
32
|
+
authenticated: true,
|
|
33
|
+
source: ctx.resolvedAuth.source,
|
|
34
|
+
masked_secret: ctx.resolvedAuth.masked_secret,
|
|
35
|
+
user_id: meResp.user_id,
|
|
36
|
+
email: meResp.email,
|
|
37
|
+
last_verified_at: new Date().toISOString(),
|
|
38
|
+
}, () => {
|
|
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
|
+
});
|
|
43
|
+
} catch (err) {
|
|
44
|
+
ctx.output({
|
|
45
|
+
ok: false,
|
|
46
|
+
authenticated: false,
|
|
47
|
+
source: ctx.resolvedAuth.source,
|
|
48
|
+
masked_secret: ctx.resolvedAuth.masked_secret,
|
|
49
|
+
error: err.message,
|
|
50
|
+
}, () => {
|
|
51
|
+
console.log(`❌ Credential verification failed: ${err.message}`);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
case 'mcp-config': {
|
|
58
|
+
const deviceEnv = getDeviceEnv();
|
|
59
|
+
if (!deviceEnv) throw new Error('shiplens.env not found. Run shiplens auth bind and complete email activation first.');
|
|
60
|
+
const client = flags.client || 'manual';
|
|
61
|
+
if (!CLIENTS.includes(client)) throw new Error(`Unsupported MCP client: ${client}. Supported: ${CLIENTS.join(', ')}`);
|
|
62
|
+
const config = getMcpConfig(client, deviceEnv.SHIPLENS_MCP_URL);
|
|
63
|
+
ctx.output({ ok: true, mcp_config: config, oauth_required: true }, () => console.log(JSON.stringify(config, null, 2)));
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
case 'configure': {
|
|
68
|
+
const deviceEnv = getDeviceEnv();
|
|
69
|
+
if (!deviceEnv) throw new Error('shiplens.env not found. Run shiplens auth bind and complete email activation first.');
|
|
70
|
+
const client = flags.client || args[0];
|
|
71
|
+
if (!CLIENTS.includes(client)) throw new Error(`Specify target MCP client via --client: ${CLIENTS.join(', ')}`);
|
|
72
|
+
const result = configureMcpClient(client, deviceEnv.SHIPLENS_MCP_URL);
|
|
73
|
+
ctx.output({ ok: true, client, oauth_required: true, ...result }, () => {
|
|
74
|
+
if (client === 'manual') {
|
|
75
|
+
console.log(JSON.stringify(result.config, null, 2));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
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
|
+
});
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
case 'set': {
|
|
85
|
+
let secret = '';
|
|
86
|
+
if (flags['secret-stdin']) {
|
|
87
|
+
const chunks = [];
|
|
88
|
+
for await (const chunk of process.stdin) {
|
|
89
|
+
chunks.push(chunk);
|
|
90
|
+
}
|
|
91
|
+
secret = Buffer.concat(chunks).toString('utf8').trim();
|
|
92
|
+
} else if (args.length > 0) {
|
|
93
|
+
secret = args[0].trim();
|
|
94
|
+
} else if (flags.secret) {
|
|
95
|
+
secret = flags.secret.trim();
|
|
96
|
+
} else {
|
|
97
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
98
|
+
secret = await new Promise((resolve) => {
|
|
99
|
+
rl.question('Enter Shiplens Access Secret: ', (ans) => {
|
|
100
|
+
rl.close();
|
|
101
|
+
resolve(ans.trim());
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!secret) {
|
|
107
|
+
throw new Error('Access Secret cannot be empty.');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const tempClient = new APIClient(ctx.client.baseURL, secret);
|
|
111
|
+
let email = '';
|
|
112
|
+
let userId = '';
|
|
113
|
+
try {
|
|
114
|
+
const meResp = await tempClient.me();
|
|
115
|
+
email = meResp.email || '';
|
|
116
|
+
userId = meResp.user_id || '';
|
|
117
|
+
} catch (e) {}
|
|
118
|
+
|
|
119
|
+
saveSecretToGlobal(secret, email, userId);
|
|
120
|
+
|
|
121
|
+
ctx.output({
|
|
122
|
+
ok: true,
|
|
123
|
+
masked_secret: maskSecret(secret),
|
|
124
|
+
email: email || undefined,
|
|
125
|
+
user_id: userId || undefined,
|
|
126
|
+
message: 'Access Secret saved securely to ~/.shiplens/config.json',
|
|
127
|
+
}, () => {
|
|
128
|
+
console.log(`✅ Access Secret saved: ${maskSecret(secret)}`);
|
|
129
|
+
if (email) console.log(`👤 Bound User: ${email} (${userId})`);
|
|
130
|
+
});
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
case 'secret': {
|
|
135
|
+
const action = args[0] || 'list';
|
|
136
|
+
if (!ctx.resolvedAuth.is_present) throw new Error('Run auth set first to configure an Access Secret with administrative rights.');
|
|
137
|
+
if (action === 'list') {
|
|
138
|
+
const response = await ctx.client.listAccessSecrets();
|
|
139
|
+
return ctx.output({ ok: true, access_secrets: response.api_keys || [] }, () => console.log(JSON.stringify(response.api_keys || [], null, 2)));
|
|
140
|
+
}
|
|
141
|
+
if (action === 'create') {
|
|
142
|
+
const scopes = flags.scopes ? String(flags.scopes).split(',').map((scope) => scope.trim()).filter(Boolean) : undefined;
|
|
143
|
+
const response = await ctx.client.createAccessSecret({ app_id: flags['app-id'], scopes, expires_at: flags.expires });
|
|
144
|
+
saveSecretToGlobal(response.api_key);
|
|
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(`✅ Offline Access Secret created and saved (${response.api_key_preview}).`);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (action === 'revoke') {
|
|
150
|
+
const apiKeyId = flags['key-id'] || args[1];
|
|
151
|
+
if (!apiKeyId) throw new Error('Provide --key-id <api_key_id>.');
|
|
152
|
+
const response = await ctx.client.revokeAccessSecret(apiKeyId);
|
|
153
|
+
return ctx.output({ ok: true, ...response }, () => console.log(`✅ Revoked Access Secret: ${response.api_key_id}`));
|
|
154
|
+
}
|
|
155
|
+
throw new Error('Supported auth secret subcommands: list, create, revoke');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
case 'whoami': {
|
|
159
|
+
const meResp = await ctx.client.me();
|
|
160
|
+
ctx.output(meResp, () => {
|
|
161
|
+
console.log(`👤 User ID: ${meResp.user_id}`);
|
|
162
|
+
console.log(`📧 Email: ${meResp.email}`);
|
|
163
|
+
if (meResp.project_count) {
|
|
164
|
+
console.log(`📦 Projects Count: ${meResp.project_count}`);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
case 'logout': {
|
|
171
|
+
clearGlobalSecret();
|
|
172
|
+
ctx.output({
|
|
173
|
+
ok: true,
|
|
174
|
+
message: 'Logged out successfully. Local credentials cleared.',
|
|
175
|
+
}, () => {
|
|
176
|
+
console.log('🚪 Logged out successfully. Local credentials cleared.');
|
|
177
|
+
});
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
case 'bind': {
|
|
182
|
+
const email = flags.email || args[0];
|
|
183
|
+
if (!email || !email.includes('@')) {
|
|
184
|
+
const err = new Error('Provide a valid email address, e.g.: shiplens auth bind --email you@company.com');
|
|
185
|
+
err.code = 'INVALID_EMAIL';
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const { getLocalConfig } = require('../config');
|
|
190
|
+
const localCfg = getLocalConfig() || {};
|
|
191
|
+
const appId = flags['app-id'] || localCfg.app_id || '';
|
|
192
|
+
const projectName = localCfg.project_name || '';
|
|
193
|
+
|
|
194
|
+
let resp;
|
|
195
|
+
try {
|
|
196
|
+
resp = await ctx.client.startEmailWithRetry({
|
|
197
|
+
email,
|
|
198
|
+
project_id: appId || undefined,
|
|
199
|
+
project_name: projectName || undefined,
|
|
200
|
+
client_name: 'Shiplens CLI',
|
|
201
|
+
device_os: process.platform,
|
|
202
|
+
device_name: require('os').hostname(),
|
|
203
|
+
});
|
|
204
|
+
} catch (err) {
|
|
205
|
+
ctx.output({
|
|
206
|
+
ok: false,
|
|
207
|
+
magic_link_sent: false,
|
|
208
|
+
email,
|
|
209
|
+
app_id: appId || undefined,
|
|
210
|
+
code: err.code || 'NETWORK_FAILED',
|
|
211
|
+
message: err.message,
|
|
212
|
+
}, () => {
|
|
213
|
+
console.error(`❌ Failed to send email [${err.code || 'NETWORK_FAILED'}]: ${err.message}`);
|
|
214
|
+
});
|
|
215
|
+
process.exitCode = 1;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const envPath = saveDeviceEnv(resp.shiplens_env);
|
|
220
|
+
|
|
221
|
+
ctx.output({
|
|
222
|
+
ok: true,
|
|
223
|
+
magic_link_sent: true,
|
|
224
|
+
email,
|
|
225
|
+
app_id: appId || undefined,
|
|
226
|
+
project_name: projectName || undefined,
|
|
227
|
+
shiplens_env_written: Boolean(envPath),
|
|
228
|
+
message: `Activation email sent to ${email}. Click the link to complete registration.`,
|
|
229
|
+
}, () => {
|
|
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.');
|
|
233
|
+
});
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
default:
|
|
238
|
+
throw new Error(`Unknown auth subcommand: ${subcommand}. Supported: status, set, secret, whoami, logout, bind, mcp-config, configure`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = { handleAuth };
|