@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/config.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const LOCAL_CONFIG_FILE = '.shiplens.json';
|
|
6
|
+
const GLOBAL_CONFIG_DIR = '.shiplens';
|
|
7
|
+
const GLOBAL_CONFIG_FILE = 'config.json';
|
|
8
|
+
const DEFAULT_API_URL = 'http://120.26.230.33';
|
|
9
|
+
const DEFAULT_ENV = 'production';
|
|
10
|
+
const DEVICE_ENV_FILE = 'shiplens.env';
|
|
11
|
+
|
|
12
|
+
function getDeviceEnv(dir = process.cwd()) {
|
|
13
|
+
const file = path.join(dir, DEVICE_ENV_FILE);
|
|
14
|
+
if (!fs.existsSync(file)) return null;
|
|
15
|
+
const values = Object.fromEntries(fs.readFileSync(file, 'utf8').split(/\r?\n/).filter(Boolean).filter((line) => !line.startsWith('#')).map((line) => line.split(/=(.*)/s).slice(0, 2)));
|
|
16
|
+
return values.SHIPLENS_MCP_TOKEN && values.SHIPLENS_MCP_URL ? values : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getLocalConfig(dir = process.cwd()) {
|
|
20
|
+
const cfgPath = path.join(dir, LOCAL_CONFIG_FILE);
|
|
21
|
+
if (fs.existsSync(cfgPath)) {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
24
|
+
} catch (e) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function saveLocalConfig(dir = process.cwd(), cfg = {}) {
|
|
32
|
+
const cfgPath = path.join(dir, LOCAL_CONFIG_FILE);
|
|
33
|
+
cfg.last_synced_at = cfg.last_synced_at || new Date().toISOString();
|
|
34
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getGlobalConfigDir() {
|
|
38
|
+
const home = os.homedir();
|
|
39
|
+
const dir = path.join(home, GLOBAL_CONFIG_DIR);
|
|
40
|
+
if (!fs.existsSync(dir)) {
|
|
41
|
+
try {
|
|
42
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
43
|
+
} catch (e) {}
|
|
44
|
+
}
|
|
45
|
+
return dir;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getGlobalConfigPath() {
|
|
49
|
+
return path.join(getGlobalConfigDir(), GLOBAL_CONFIG_FILE);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getGlobalConfig() {
|
|
53
|
+
const cfgPath = getGlobalConfigPath();
|
|
54
|
+
if (fs.existsSync(cfgPath)) {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
57
|
+
} catch (e) {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function saveGlobalConfig(cfg = {}) {
|
|
65
|
+
const cfgPath = getGlobalConfigPath();
|
|
66
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = {
|
|
70
|
+
LOCAL_CONFIG_FILE,
|
|
71
|
+
GLOBAL_CONFIG_FILE,
|
|
72
|
+
DEFAULT_API_URL,
|
|
73
|
+
DEFAULT_ENV,
|
|
74
|
+
DEVICE_ENV_FILE,
|
|
75
|
+
getDeviceEnv,
|
|
76
|
+
getLocalConfig,
|
|
77
|
+
saveLocalConfig,
|
|
78
|
+
getGlobalConfig,
|
|
79
|
+
saveGlobalConfig,
|
|
80
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function saveDeviceEnv(envText, dir = process.cwd()) {
|
|
5
|
+
if (!envText) return null;
|
|
6
|
+
const envPath = path.join(dir, 'shiplens.env');
|
|
7
|
+
fs.writeFileSync(envPath, envText, { mode: 0o600 });
|
|
8
|
+
const ignorePath = path.join(dir, '.gitignore');
|
|
9
|
+
const ignored = fs.existsSync(ignorePath) ? fs.readFileSync(ignorePath, 'utf8') : '';
|
|
10
|
+
if (!ignored.split(/\r?\n/).includes('shiplens.env')) {
|
|
11
|
+
fs.appendFileSync(ignorePath, `${ignored && !ignored.endsWith('\n') ? '\n' : ''}shiplens.env\n`);
|
|
12
|
+
}
|
|
13
|
+
return envPath;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = { saveDeviceEnv };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const { runCLI, parseArgs, VERSION } = require('./cli');
|
|
2
|
+
const { APIClient, ERROR_CODES } = require('./api');
|
|
3
|
+
const { detectProject, injectSDK } = require('./injector');
|
|
4
|
+
const { getLocalConfig, saveLocalConfig, getGlobalConfig, saveGlobalConfig } = require('./config');
|
|
5
|
+
const { resolveSecret, maskSecret } = require('./auth');
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
runCLI,
|
|
9
|
+
parseArgs,
|
|
10
|
+
VERSION,
|
|
11
|
+
APIClient,
|
|
12
|
+
ERROR_CODES,
|
|
13
|
+
detectProject,
|
|
14
|
+
injectSDK,
|
|
15
|
+
getLocalConfig,
|
|
16
|
+
saveLocalConfig,
|
|
17
|
+
getGlobalConfig,
|
|
18
|
+
saveGlobalConfig,
|
|
19
|
+
resolveSecret,
|
|
20
|
+
maskSecret,
|
|
21
|
+
};
|
package/lib/injector.js
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
const FRAMEWORKS = {
|
|
6
|
+
NEXT_APP: 'nextjs-app',
|
|
7
|
+
NEXT_PAGES: 'nextjs-pages',
|
|
8
|
+
VITE: 'vite',
|
|
9
|
+
VUE: 'vue',
|
|
10
|
+
HTML: 'html',
|
|
11
|
+
UNKNOWN: 'unknown',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function detectPackageManager(dir = process.cwd()) {
|
|
15
|
+
if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
16
|
+
if (fs.existsSync(path.join(dir, 'yarn.lock'))) return 'yarn';
|
|
17
|
+
if (fs.existsSync(path.join(dir, 'bun.lockb')) || fs.existsSync(path.join(dir, 'bun.lock'))) return 'bun';
|
|
18
|
+
return 'npm';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { inferTaxonomy } = require('./taxonomy');
|
|
22
|
+
|
|
23
|
+
function detectProject(dir = process.cwd()) {
|
|
24
|
+
const result = {
|
|
25
|
+
framework: FRAMEWORKS.UNKNOWN,
|
|
26
|
+
package_manager: detectPackageManager(dir),
|
|
27
|
+
project_name: path.basename(dir),
|
|
28
|
+
industry: 'saas',
|
|
29
|
+
description: '',
|
|
30
|
+
keywords: [],
|
|
31
|
+
dependencies: {},
|
|
32
|
+
devDependencies: {},
|
|
33
|
+
taxonomy: null,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
37
|
+
if (fs.existsSync(pkgPath)) {
|
|
38
|
+
try {
|
|
39
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
40
|
+
if (pkg.name) result.project_name = pkg.name;
|
|
41
|
+
if (pkg.description) result.description = pkg.description;
|
|
42
|
+
if (Array.isArray(pkg.keywords)) result.keywords = pkg.keywords;
|
|
43
|
+
if (pkg.dependencies) result.dependencies = pkg.dependencies;
|
|
44
|
+
if (pkg.devDependencies) result.devDependencies = pkg.devDependencies;
|
|
45
|
+
|
|
46
|
+
const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
|
|
47
|
+
if (deps['next']) {
|
|
48
|
+
if (
|
|
49
|
+
fs.existsSync(path.join(dir, 'src/app/layout.tsx')) ||
|
|
50
|
+
fs.existsSync(path.join(dir, 'app/layout.tsx')) ||
|
|
51
|
+
fs.existsSync(path.join(dir, 'src/app/layout.js')) ||
|
|
52
|
+
fs.existsSync(path.join(dir, 'app/layout.js')) ||
|
|
53
|
+
fs.existsSync(path.join(dir, 'src/app/layout.jsx')) ||
|
|
54
|
+
fs.existsSync(path.join(dir, 'app/layout.jsx'))
|
|
55
|
+
) {
|
|
56
|
+
result.framework = FRAMEWORKS.NEXT_APP;
|
|
57
|
+
} else {
|
|
58
|
+
result.framework = FRAMEWORKS.NEXT_PAGES;
|
|
59
|
+
}
|
|
60
|
+
} else if (deps['vue'] || deps['nuxt']) {
|
|
61
|
+
result.framework = FRAMEWORKS.VUE;
|
|
62
|
+
} else if (deps['vite'] || deps['react'] || deps['svelte']) {
|
|
63
|
+
result.framework = FRAMEWORKS.VITE;
|
|
64
|
+
}
|
|
65
|
+
} catch (e) {}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (result.framework === FRAMEWORKS.UNKNOWN) {
|
|
69
|
+
if (
|
|
70
|
+
fs.existsSync(path.join(dir, 'src/app/layout.tsx')) ||
|
|
71
|
+
fs.existsSync(path.join(dir, 'app/layout.tsx')) ||
|
|
72
|
+
fs.existsSync(path.join(dir, 'src/app/layout.js')) ||
|
|
73
|
+
fs.existsSync(path.join(dir, 'app/layout.js'))
|
|
74
|
+
) {
|
|
75
|
+
result.framework = FRAMEWORKS.NEXT_APP;
|
|
76
|
+
} else if (
|
|
77
|
+
fs.existsSync(path.join(dir, 'src/pages/_app.tsx')) ||
|
|
78
|
+
fs.existsSync(path.join(dir, 'pages/_app.tsx')) ||
|
|
79
|
+
fs.existsSync(path.join(dir, 'src/pages/_app.js')) ||
|
|
80
|
+
fs.existsSync(path.join(dir, 'pages/_app.js'))
|
|
81
|
+
) {
|
|
82
|
+
result.framework = FRAMEWORKS.NEXT_PAGES;
|
|
83
|
+
} else if (
|
|
84
|
+
fs.existsSync(path.join(dir, 'vite.config.ts')) ||
|
|
85
|
+
fs.existsSync(path.join(dir, 'vite.config.js')) ||
|
|
86
|
+
fs.existsSync(path.join(dir, 'src/main.tsx')) ||
|
|
87
|
+
fs.existsSync(path.join(dir, 'src/main.ts'))
|
|
88
|
+
) {
|
|
89
|
+
result.framework = FRAMEWORKS.VITE;
|
|
90
|
+
} else if (fs.existsSync(path.join(dir, 'index.html')) || fs.existsSync(path.join(dir, 'public/index.html'))) {
|
|
91
|
+
result.framework = FRAMEWORKS.HTML;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Extract description fallback from README if missing
|
|
96
|
+
if (!result.description) {
|
|
97
|
+
const readmeCandidates = ['README.md', 'readme.md', 'README.MD'];
|
|
98
|
+
for (const r of readmeCandidates) {
|
|
99
|
+
const p = path.join(dir, r);
|
|
100
|
+
if (fs.existsSync(p)) {
|
|
101
|
+
try {
|
|
102
|
+
const content = fs.readFileSync(p, 'utf8');
|
|
103
|
+
const lines = content.split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#') && !l.startsWith('!'));
|
|
104
|
+
if (lines.length > 0) {
|
|
105
|
+
result.description = lines[0].slice(0, 200);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
} catch (e) {}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 4-level taxonomy inference
|
|
114
|
+
const taxonomy = inferTaxonomy({
|
|
115
|
+
name: result.project_name,
|
|
116
|
+
description: result.description,
|
|
117
|
+
keywords: result.keywords,
|
|
118
|
+
dependencies: result.dependencies,
|
|
119
|
+
devDependencies: result.devDependencies,
|
|
120
|
+
framework: result.framework,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
result.taxonomy = taxonomy;
|
|
124
|
+
result.industry = taxonomy.subgenre.id || taxonomy.genre.id || 'saas';
|
|
125
|
+
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function injectNextApp(dir, appId) {
|
|
130
|
+
const candidates = [
|
|
131
|
+
'src/app/layout.tsx', 'app/layout.tsx',
|
|
132
|
+
'src/app/layout.jsx', 'app/layout.jsx',
|
|
133
|
+
'src/app/layout.js', 'app/layout.js',
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
let targetPath = '';
|
|
137
|
+
let relPath = '';
|
|
138
|
+
for (const cand of candidates) {
|
|
139
|
+
const p = path.join(dir, cand);
|
|
140
|
+
if (fs.existsSync(p)) {
|
|
141
|
+
targetPath = p;
|
|
142
|
+
relPath = cand;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!targetPath) {
|
|
148
|
+
const compDir = fs.existsSync(path.join(dir, 'src')) ? path.join(dir, 'src/components') : path.join(dir, 'components');
|
|
149
|
+
fs.mkdirSync(compDir, { recursive: true });
|
|
150
|
+
const compPath = path.join(compDir, 'ShiplensTracker.tsx');
|
|
151
|
+
const compContent = `'use client';\n\nimport { useEffect } from 'react';\nimport { initShiplens } from '@shiplens/sdk';\n\nexport function ShiplensTracker() {\n useEffect(() => {\n initShiplens({ appId: '${appId}' });\n }, []);\n return null;\n}\n`;
|
|
152
|
+
fs.writeFileSync(compPath, compContent, 'utf8');
|
|
153
|
+
return path.relative(dir, compPath).replace(/\\/g, '/');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let content = fs.readFileSync(targetPath, 'utf8');
|
|
157
|
+
if (content.includes('@shiplens/sdk') || content.includes('initShiplens') || content.includes(appId)) {
|
|
158
|
+
return relPath;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (content.includes("'use client'") || content.includes('"use client"')) {
|
|
162
|
+
const newContent = `import { initShiplens } from '@shiplens/sdk';\n\nif (typeof window !== 'undefined') {\n initShiplens({ appId: '${appId}' });\n}\n` + content;
|
|
163
|
+
fs.writeFileSync(targetPath, newContent, 'utf8');
|
|
164
|
+
return relPath;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const compDir = path.dirname(targetPath);
|
|
168
|
+
const ext = targetPath.endsWith('.js') || targetPath.endsWith('.jsx') ? '.jsx' : '.tsx';
|
|
169
|
+
const trackerFile = path.join(compDir, `ShiplensTracker${ext}`);
|
|
170
|
+
const trackerContent = `'use client';\n\nimport { useEffect } from 'react';\nimport { initShiplens } from '@shiplens/sdk';\n\nexport function ShiplensTracker() {\n useEffect(() => {\n initShiplens({ appId: '${appId}' });\n }, []);\n return null;\n}\n`;
|
|
171
|
+
fs.writeFileSync(trackerFile, trackerContent, 'utf8');
|
|
172
|
+
|
|
173
|
+
if (!content.includes('ShiplensTracker')) {
|
|
174
|
+
content = `import { ShiplensTracker } from './ShiplensTracker';\n` + content;
|
|
175
|
+
if (content.includes('{children}')) {
|
|
176
|
+
content = content.replace('{children}', '{children}\n <ShiplensTracker />');
|
|
177
|
+
}
|
|
178
|
+
fs.writeFileSync(targetPath, content, 'utf8');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return path.relative(dir, trackerFile).replace(/\\/g, '/');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function injectNextPages(dir, appId) {
|
|
185
|
+
const candidates = [
|
|
186
|
+
'src/pages/_app.tsx', 'pages/_app.tsx',
|
|
187
|
+
'src/pages/_app.jsx', 'pages/_app.jsx',
|
|
188
|
+
'src/pages/_app.js', 'pages/_app.js',
|
|
189
|
+
];
|
|
190
|
+
for (const cand of candidates) {
|
|
191
|
+
const p = path.join(dir, cand);
|
|
192
|
+
if (fs.existsSync(p)) {
|
|
193
|
+
let content = fs.readFileSync(p, 'utf8');
|
|
194
|
+
if (content.includes('@shiplens/sdk') || content.includes('initShiplens')) {
|
|
195
|
+
return cand;
|
|
196
|
+
}
|
|
197
|
+
const injection = `import { useEffect } from 'react';\nimport { initShiplens } from '@shiplens/sdk';\n\nif (typeof window !== 'undefined') {\n initShiplens({ appId: '${appId}' });\n}\n`;
|
|
198
|
+
fs.writeFileSync(p, injection + '\n' + content, 'utf8');
|
|
199
|
+
return cand;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return injectFallback(dir, appId);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function injectVite(dir, appId) {
|
|
206
|
+
const candidates = [
|
|
207
|
+
'src/main.tsx', 'src/main.ts',
|
|
208
|
+
'src/main.jsx', 'src/main.js',
|
|
209
|
+
'src/index.tsx', 'src/index.ts',
|
|
210
|
+
'src/index.jsx', 'src/index.js',
|
|
211
|
+
];
|
|
212
|
+
for (const cand of candidates) {
|
|
213
|
+
const p = path.join(dir, cand);
|
|
214
|
+
if (fs.existsSync(p)) {
|
|
215
|
+
let content = fs.readFileSync(p, 'utf8');
|
|
216
|
+
if (content.includes('@shiplens/sdk') || content.includes('initShiplens')) {
|
|
217
|
+
return cand;
|
|
218
|
+
}
|
|
219
|
+
const injection = `import { initShiplens } from '@shiplens/sdk';\n\ninitShiplens({ appId: '${appId}' });\n`;
|
|
220
|
+
fs.writeFileSync(p, injection + '\n' + content, 'utf8');
|
|
221
|
+
return cand;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return injectFallback(dir, appId);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function injectVue(dir, appId) {
|
|
228
|
+
const candidates = ['src/main.ts', 'src/main.js', 'src/App.vue'];
|
|
229
|
+
for (const cand of candidates) {
|
|
230
|
+
const p = path.join(dir, cand);
|
|
231
|
+
if (fs.existsSync(p)) {
|
|
232
|
+
let content = fs.readFileSync(p, 'utf8');
|
|
233
|
+
if (content.includes('@shiplens/sdk') || content.includes('initShiplens')) {
|
|
234
|
+
return cand;
|
|
235
|
+
}
|
|
236
|
+
if (cand.endsWith('.vue') && content.includes('<script setup')) {
|
|
237
|
+
const newScript = `<script setup>\nimport { onMounted } from 'vue';\nimport { initShiplens } from '@shiplens/sdk';\n\nonMounted(() => {\n initShiplens({ appId: '${appId}' });\n});\n`;
|
|
238
|
+
content = content.replace('<script setup>', newScript);
|
|
239
|
+
fs.writeFileSync(p, content, 'utf8');
|
|
240
|
+
return cand;
|
|
241
|
+
}
|
|
242
|
+
const injection = `import { initShiplens } from '@shiplens/sdk';\n\ninitShiplens({ appId: '${appId}' });\n`;
|
|
243
|
+
fs.writeFileSync(p, injection + '\n' + content, 'utf8');
|
|
244
|
+
return cand;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return injectFallback(dir, appId);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function injectHTML(dir, appId) {
|
|
251
|
+
const candidates = ['index.html', 'public/index.html', 'src/index.html'];
|
|
252
|
+
for (const cand of candidates) {
|
|
253
|
+
const p = path.join(dir, cand);
|
|
254
|
+
if (fs.existsSync(p)) {
|
|
255
|
+
let content = fs.readFileSync(p, 'utf8');
|
|
256
|
+
if (content.includes('cdn.shiplens.dev/sdk.js') || content.includes(appId)) {
|
|
257
|
+
return cand;
|
|
258
|
+
}
|
|
259
|
+
const scriptTag = ` <script src="https://cdn.shiplens.dev/sdk.js" data-app-id="${appId}" defer></script>\n`;
|
|
260
|
+
if (content.includes('</head>')) {
|
|
261
|
+
content = content.replace('</head>', `${scriptTag} </head>`);
|
|
262
|
+
} else if (content.includes('</body>')) {
|
|
263
|
+
content = content.replace('</body>', `${scriptTag} </body>`);
|
|
264
|
+
} else {
|
|
265
|
+
content += '\n' + scriptTag;
|
|
266
|
+
}
|
|
267
|
+
fs.writeFileSync(p, content, 'utf8');
|
|
268
|
+
return cand;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return injectFallback(dir, appId);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function injectFallback(dir, appId) {
|
|
275
|
+
const configFile = path.join(dir, 'shiplens.config.ts');
|
|
276
|
+
const content = `import { initShiplens } from '@shiplens/sdk';\n\nexport function setupShiplens() {\n if (typeof window !== 'undefined') {\n initShiplens({\n appId: '${appId}',\n });\n }\n}\n`;
|
|
277
|
+
fs.writeFileSync(configFile, content, 'utf8');
|
|
278
|
+
return 'shiplens.config.ts';
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function injectSDK(dir, framework, appId) {
|
|
282
|
+
switch (framework) {
|
|
283
|
+
case FRAMEWORKS.NEXT_APP:
|
|
284
|
+
return injectNextApp(dir, appId);
|
|
285
|
+
case FRAMEWORKS.NEXT_PAGES:
|
|
286
|
+
return injectNextPages(dir, appId);
|
|
287
|
+
case FRAMEWORKS.VUE:
|
|
288
|
+
return injectVue(dir, appId);
|
|
289
|
+
case FRAMEWORKS.VITE:
|
|
290
|
+
return injectVite(dir, appId);
|
|
291
|
+
case FRAMEWORKS.HTML:
|
|
292
|
+
return injectHTML(dir, appId);
|
|
293
|
+
default:
|
|
294
|
+
try { return injectNextApp(dir, appId); } catch (e) {}
|
|
295
|
+
try { return injectVite(dir, appId); } catch (e) {}
|
|
296
|
+
try { return injectHTML(dir, appId); } catch (e) {}
|
|
297
|
+
return injectFallback(dir, appId);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function installSDKDependency(dir, pkgManager = 'npm') {
|
|
302
|
+
const isWindows = process.platform === 'win32';
|
|
303
|
+
const ext = isWindows ? '.cmd' : '';
|
|
304
|
+
|
|
305
|
+
// 1. Define 3-Tier Download Sources (npm registry -> GitHub mirror -> Shiplens CDN mirror)
|
|
306
|
+
const sources = [
|
|
307
|
+
{
|
|
308
|
+
name: 'npm registry',
|
|
309
|
+
getCommand: (pm) => {
|
|
310
|
+
if (pm === 'pnpm') return `pnpm${ext} add @shiplens/sdk`;
|
|
311
|
+
if (pm === 'yarn') return `yarn${ext} add @shiplens/sdk`;
|
|
312
|
+
if (pm === 'bun') return `bun${ext} add @shiplens/sdk`;
|
|
313
|
+
return `npm${ext} install @shiplens/sdk --legacy-peer-deps --no-audit --no-fund`;
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: 'GitHub mirror',
|
|
318
|
+
getCommand: (pm) => {
|
|
319
|
+
const repoUrl = 'https://github.com/Hyperlong/shiplens-sdk.git';
|
|
320
|
+
if (pm === 'pnpm') return `pnpm${ext} add ${repoUrl}`;
|
|
321
|
+
if (pm === 'yarn') return `yarn${ext} add ${repoUrl}`;
|
|
322
|
+
if (pm === 'bun') return `bun${ext} add ${repoUrl}`;
|
|
323
|
+
return `npm${ext} install ${repoUrl} --legacy-peer-deps --no-audit --no-fund`;
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: 'Shiplens CDN mirror',
|
|
328
|
+
getCommand: (pm) => {
|
|
329
|
+
const cdnUrl = 'https://cdn.shiplens.dev/packages/shiplens-sdk.tgz';
|
|
330
|
+
if (pm === 'pnpm') return `pnpm${ext} add ${cdnUrl}`;
|
|
331
|
+
if (pm === 'yarn') return `yarn${ext} add ${cdnUrl}`;
|
|
332
|
+
if (pm === 'bun') return `bun${ext} add ${cdnUrl}`;
|
|
333
|
+
return `npm${ext} install ${cdnUrl} --legacy-peer-deps --no-audit --no-fund`;
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
];
|
|
337
|
+
|
|
338
|
+
let lastError = null;
|
|
339
|
+
|
|
340
|
+
// Try each source with strict 10s (10000ms) timeout
|
|
341
|
+
for (const source of sources) {
|
|
342
|
+
const cmd = source.getCommand(pkgManager);
|
|
343
|
+
try {
|
|
344
|
+
execSync(cmd, { cwd: dir, stdio: 'ignore', timeout: 10000 });
|
|
345
|
+
return { success: true, manager: pkgManager, source: source.name };
|
|
346
|
+
} catch (err) {
|
|
347
|
+
lastError = err;
|
|
348
|
+
// If primary package manager failed, also try npm fallback on that source
|
|
349
|
+
if (pkgManager !== 'npm') {
|
|
350
|
+
const npmCmd = source.getCommand('npm');
|
|
351
|
+
try {
|
|
352
|
+
execSync(npmCmd, { cwd: dir, stdio: 'ignore', timeout: 10000 });
|
|
353
|
+
return { success: true, manager: 'npm (fallback)', source: source.name };
|
|
354
|
+
} catch (npmErr) {
|
|
355
|
+
lastError = npmErr;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
success: false,
|
|
363
|
+
manager: pkgManager,
|
|
364
|
+
error: lastError ? lastError.message : 'All 3 download sources timed out or failed',
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function getGitEmail() {
|
|
369
|
+
try {
|
|
370
|
+
return execSync('git config user.email', { encoding: 'utf8' }).trim();
|
|
371
|
+
} catch (e) {
|
|
372
|
+
return '';
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function injectSkill(dir = process.cwd()) {
|
|
377
|
+
const { SKILL_CONTENT } = require('./assets/skill');
|
|
378
|
+
|
|
379
|
+
// 1. Generate generic Agent Skill (.agents/skills/shiplens/SKILL.md)
|
|
380
|
+
const agentSkillDir = path.join(dir, '.agents', 'skills', 'shiplens');
|
|
381
|
+
try {
|
|
382
|
+
fs.mkdirSync(agentSkillDir, { recursive: true });
|
|
383
|
+
fs.writeFileSync(path.join(agentSkillDir, 'SKILL.md'), SKILL_CONTENT, 'utf8');
|
|
384
|
+
} catch (e) {}
|
|
385
|
+
|
|
386
|
+
// 2. Generate Cursor rule (.cursor/rules/shiplens.mdc)
|
|
387
|
+
const cursorRulesDir = path.join(dir, '.cursor', 'rules');
|
|
388
|
+
try {
|
|
389
|
+
fs.mkdirSync(cursorRulesDir, { recursive: true });
|
|
390
|
+
fs.writeFileSync(path.join(cursorRulesDir, 'shiplens.mdc'), SKILL_CONTENT, 'utf8');
|
|
391
|
+
} catch (e) {}
|
|
392
|
+
|
|
393
|
+
return '.agents/skills/shiplens/SKILL.md';
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function detectExistingApp(dir = process.cwd()) {
|
|
397
|
+
// 1. Check local .shiplens.json state machine
|
|
398
|
+
const cfgPath = path.join(dir, '.shiplens.json');
|
|
399
|
+
if (fs.existsSync(cfgPath)) {
|
|
400
|
+
try {
|
|
401
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
402
|
+
if (cfg && cfg.app_id) {
|
|
403
|
+
return {
|
|
404
|
+
has_existing: true,
|
|
405
|
+
app_id: cfg.app_id,
|
|
406
|
+
source_file: '.shiplens.json',
|
|
407
|
+
project_name: cfg.project_name || '',
|
|
408
|
+
industry: cfg.industry || '',
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
} catch (e) {}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// 2. Check code files for existing instrumentation
|
|
415
|
+
const checkFiles = [
|
|
416
|
+
'src/app/layout.tsx', 'app/layout.tsx',
|
|
417
|
+
'src/app/layout.jsx', 'app/layout.jsx',
|
|
418
|
+
'src/app/layout.js', 'app/layout.js',
|
|
419
|
+
'src/pages/_app.tsx', 'pages/_app.tsx',
|
|
420
|
+
'src/pages/_app.jsx', 'pages/_app.jsx',
|
|
421
|
+
'src/pages/_app.js', 'pages/_app.js',
|
|
422
|
+
'src/main.tsx', 'src/main.ts',
|
|
423
|
+
'src/main.jsx', 'src/main.js',
|
|
424
|
+
'src/index.tsx', 'src/index.ts',
|
|
425
|
+
'src/index.jsx', 'src/index.js',
|
|
426
|
+
'src/App.vue', 'src/App.tsx', 'src/App.jsx',
|
|
427
|
+
'index.html', 'public/index.html', 'src/index.html',
|
|
428
|
+
'shiplens.config.ts', 'shiplens.config.js',
|
|
429
|
+
'src/components/ShiplensTracker.tsx', 'components/ShiplensTracker.tsx',
|
|
430
|
+
'src/components/ShiplensTracker.jsx', 'components/ShiplensTracker.jsx',
|
|
431
|
+
];
|
|
432
|
+
|
|
433
|
+
for (const rel of checkFiles) {
|
|
434
|
+
const p = path.join(dir, rel);
|
|
435
|
+
if (fs.existsSync(p)) {
|
|
436
|
+
try {
|
|
437
|
+
const content = fs.readFileSync(p, 'utf8');
|
|
438
|
+
const matchAppId = content.match(/appId\s*:\s*['"]([^'"]+)['"]/);
|
|
439
|
+
if (matchAppId && matchAppId[1]) {
|
|
440
|
+
return {
|
|
441
|
+
has_existing: true,
|
|
442
|
+
app_id: matchAppId[1],
|
|
443
|
+
source_file: rel,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
const matchDataAppId = content.match(/data-app-id\s*=\s*['"]([^'"]+)['"]/);
|
|
447
|
+
if (matchDataAppId && matchDataAppId[1]) {
|
|
448
|
+
return {
|
|
449
|
+
has_existing: true,
|
|
450
|
+
app_id: matchDataAppId[1],
|
|
451
|
+
source_file: rel,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
if (content.includes('@shiplens/sdk') || content.includes('cdn.shiplens.dev/sdk.js') || content.includes('initShiplens')) {
|
|
455
|
+
return {
|
|
456
|
+
has_existing: true,
|
|
457
|
+
app_id: 'unknown_local_id',
|
|
458
|
+
source_file: rel,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
} catch (e) {}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// 3. Check package.json dependencies
|
|
466
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
467
|
+
if (fs.existsSync(pkgPath)) {
|
|
468
|
+
try {
|
|
469
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
470
|
+
const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
|
|
471
|
+
if (deps['@shiplens/sdk']) {
|
|
472
|
+
return {
|
|
473
|
+
has_existing: true,
|
|
474
|
+
app_id: 'configured_in_sdk',
|
|
475
|
+
source_file: 'package.json',
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
} catch (e) {}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return { has_existing: false };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function autoGitCommit(dir = process.cwd(), message = 'feat: Integrate Shiplens SDK and generate configuration & dashboards') {
|
|
485
|
+
try {
|
|
486
|
+
execSync('git rev-parse --is-inside-work-tree', { cwd: dir, stdio: 'ignore' });
|
|
487
|
+
const status = execSync('git status --porcelain', { cwd: dir, encoding: 'utf8' }).trim();
|
|
488
|
+
if (status) {
|
|
489
|
+
execSync('git add .', { cwd: dir, stdio: 'ignore' });
|
|
490
|
+
execSync(`git commit -m "${message}"`, { cwd: dir, stdio: 'ignore' });
|
|
491
|
+
const hash = execSync('git rev-parse --short HEAD', { cwd: dir, encoding: 'utf8' }).trim();
|
|
492
|
+
return { committed: true, hash, message };
|
|
493
|
+
}
|
|
494
|
+
} catch (e) {
|
|
495
|
+
return { committed: false, error: e.message };
|
|
496
|
+
}
|
|
497
|
+
return { committed: false, reason: 'no_changes' };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
module.exports = {
|
|
501
|
+
FRAMEWORKS,
|
|
502
|
+
detectPackageManager,
|
|
503
|
+
detectProject,
|
|
504
|
+
detectExistingApp,
|
|
505
|
+
injectSDK,
|
|
506
|
+
injectSkill,
|
|
507
|
+
installSDKDependency,
|
|
508
|
+
getGitEmail,
|
|
509
|
+
autoGitCommit,
|
|
510
|
+
};
|