servicemap 0.1.0
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 +51 -0
- package/bin/servicemap.mjs +98 -0
- package/package.json +34 -0
- package/servicemap/SKILL.md +40 -0
- package/servicemap/agents/openai.yaml +3 -0
- package/servicemap/assets/service-logo-database.json +13194 -0
- package/servicemap/references/desktop-fallback.md +29 -0
- package/servicemap/references/framework-routing.md +16 -0
- package/servicemap/references/page-design.md +22 -0
- package/servicemap/references/service-detection-rules.md +22 -0
- package/servicemap/scripts/generate-desktop-app.mjs +411 -0
- package/servicemap/scripts/generate-page.mjs +184 -0
- package/servicemap/scripts/scan-project.mjs +290 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Desktop Fallback
|
|
2
|
+
|
|
3
|
+
Use the desktop fallback when no website route can be confidently attached.
|
|
4
|
+
|
|
5
|
+
Run:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
scripts/generate-desktop-app.mjs --root <project-root>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The generated app is written to:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
servicemap-desktop
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
It is a Vite React app with Tauri configuration.
|
|
18
|
+
It can run as a web preview with `npm run dev`.
|
|
19
|
+
It can be packaged as a native desktop app with `npm run tauri build` when the local machine has the Tauri/Rust toolchain available.
|
|
20
|
+
|
|
21
|
+
Validate the generated app with:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install
|
|
25
|
+
npm run build
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Do not install global tooling automatically.
|
|
29
|
+
If native packaging fails because Rust, platform SDKs, or Tauri prerequisites are missing, report that the app source was generated and the web build passed.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Framework Routing
|
|
2
|
+
|
|
3
|
+
Create `/internalservicemap` using the project's existing routing convention.
|
|
4
|
+
|
|
5
|
+
Preferred route locations:
|
|
6
|
+
- Next.js App Router: `app/internalservicemap/page.tsx` or `src/app/internalservicemap/page.tsx`.
|
|
7
|
+
- Next.js Pages Router: `pages/internalservicemap.tsx` or `src/pages/internalservicemap.tsx`.
|
|
8
|
+
- Astro: `src/pages/internalservicemap.astro`.
|
|
9
|
+
- Remix: `app/routes/internalservicemap.tsx`.
|
|
10
|
+
- Vite apps with an existing router: add a route matching the app's route conventions.
|
|
11
|
+
- Unknown static sites: prefer the desktop fallback app unless the user explicitly wants static website files.
|
|
12
|
+
|
|
13
|
+
If the project has middleware, auth wrappers, layout conventions, or route metadata, adapt the generated page to match.
|
|
14
|
+
Do not invent a new router or add a new framework.
|
|
15
|
+
|
|
16
|
+
If a project has no website, run `scripts/generate-desktop-app.mjs --root <project-root>` and report the generated desktop app path.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Page Design
|
|
2
|
+
|
|
3
|
+
The page should be useful on first load, not a marketing landing page.
|
|
4
|
+
|
|
5
|
+
Show:
|
|
6
|
+
- Project name.
|
|
7
|
+
- Number of detected services.
|
|
8
|
+
- High-confidence service count.
|
|
9
|
+
- Framework detected.
|
|
10
|
+
- Service cards with logo, name, category, confidence, and strongest evidence.
|
|
11
|
+
|
|
12
|
+
Keep the text concise.
|
|
13
|
+
Use the host project's design system when it is easy to identify.
|
|
14
|
+
Avoid exposing secrets or internal URLs that are not already present as public hostnames.
|
|
15
|
+
|
|
16
|
+
Prefer a clean operational interface:
|
|
17
|
+
- compact summary metrics
|
|
18
|
+
- scannable service cards
|
|
19
|
+
- clear confidence labels
|
|
20
|
+
- evidence shown in short form
|
|
21
|
+
|
|
22
|
+
If the UI looks obviously off after rendering, fix the layout before finishing.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Service Detection Rules
|
|
2
|
+
|
|
3
|
+
The scanner should prefer evidence from package dependencies, imports, known config files, environment variable names, and official service domains.
|
|
4
|
+
|
|
5
|
+
High-confidence evidence:
|
|
6
|
+
- Direct dependency names such as `stripe`, `firebase`, `@supabase/supabase-js`, or `@sentry/nextjs`.
|
|
7
|
+
- Known config files such as `firebase.json`, `vercel.json`, or `wrangler.toml`.
|
|
8
|
+
|
|
9
|
+
Medium-confidence evidence:
|
|
10
|
+
- Environment variable names such as `STRIPE_SECRET_KEY`, `RESEND_API_KEY`, or `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`.
|
|
11
|
+
- Official domains found in docs or source comments.
|
|
12
|
+
|
|
13
|
+
Low-confidence evidence:
|
|
14
|
+
- Generic text mentions without package, env, URL, or config evidence.
|
|
15
|
+
|
|
16
|
+
Never inspect or display secret values.
|
|
17
|
+
Only read the variable name before the equals sign in `.env` files.
|
|
18
|
+
|
|
19
|
+
False positives to watch:
|
|
20
|
+
- Short generated-database aliases that match ordinary words.
|
|
21
|
+
- Package names that are not SaaS services.
|
|
22
|
+
- Documentation examples that mention services the project does not actually use.
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const defaultInput = '.servicemap/internal-servicemap.json';
|
|
6
|
+
const defaultOutput = 'servicemap-desktop';
|
|
7
|
+
|
|
8
|
+
function parseArgs() {
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
const options = { root: process.cwd(), input: defaultInput, output: defaultOutput };
|
|
11
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
12
|
+
if (args[index] === '--root') options.root = args[index + 1] ?? options.root;
|
|
13
|
+
if (args[index] === '--input') options.input = args[index + 1] ?? options.input;
|
|
14
|
+
if (args[index] === '--output') options.output = args[index + 1] ?? options.output;
|
|
15
|
+
}
|
|
16
|
+
options.root = resolve(options.root);
|
|
17
|
+
options.input = resolve(options.root, options.input);
|
|
18
|
+
options.output = resolve(options.root, options.output);
|
|
19
|
+
return options;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function readJson(path) {
|
|
23
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function writeProjectFile(root, path, contents) {
|
|
27
|
+
const filePath = join(root, path);
|
|
28
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
29
|
+
await writeFile(filePath, contents);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function packageJson(graph) {
|
|
33
|
+
const appName = `${graph.project?.name ?? 'internal'}-servicemap`.replace(/[^a-z0-9-]+/gi, '-').toLowerCase();
|
|
34
|
+
return `${JSON.stringify({
|
|
35
|
+
name: appName,
|
|
36
|
+
private: true,
|
|
37
|
+
version: '0.1.0',
|
|
38
|
+
type: 'module',
|
|
39
|
+
scripts: {
|
|
40
|
+
dev: 'vite',
|
|
41
|
+
build: 'vite build',
|
|
42
|
+
preview: 'vite preview',
|
|
43
|
+
tauri: 'tauri',
|
|
44
|
+
},
|
|
45
|
+
dependencies: {
|
|
46
|
+
'@vitejs/plugin-react': '^5.0.0',
|
|
47
|
+
vite: '^7.0.0',
|
|
48
|
+
typescript: '^5.0.0',
|
|
49
|
+
react: '^19.0.0',
|
|
50
|
+
'react-dom': '^19.0.0',
|
|
51
|
+
'@tauri-apps/api': '^2.0.0',
|
|
52
|
+
},
|
|
53
|
+
devDependencies: {
|
|
54
|
+
'@tauri-apps/cli': '^2.0.0',
|
|
55
|
+
},
|
|
56
|
+
}, null, 2)}\n`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function viteConfig() {
|
|
60
|
+
return `import { defineConfig } from 'vite';
|
|
61
|
+
import react from '@vitejs/plugin-react';
|
|
62
|
+
|
|
63
|
+
export default defineConfig({
|
|
64
|
+
plugins: [react()],
|
|
65
|
+
server: {
|
|
66
|
+
port: 5173,
|
|
67
|
+
strictPort: false,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function indexHtml() {
|
|
74
|
+
return `<!doctype html>
|
|
75
|
+
<html lang="en">
|
|
76
|
+
<head>
|
|
77
|
+
<meta charset="UTF-8" />
|
|
78
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
79
|
+
<title>Internal Servicemap</title>
|
|
80
|
+
</head>
|
|
81
|
+
<body>
|
|
82
|
+
<div id="root"></div>
|
|
83
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
84
|
+
</body>
|
|
85
|
+
</html>
|
|
86
|
+
`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function mainJsx() {
|
|
90
|
+
return `import React from 'react';
|
|
91
|
+
import { createRoot } from 'react-dom/client';
|
|
92
|
+
import graph from '.servicemap-data.json';
|
|
93
|
+
import './styles.css';
|
|
94
|
+
|
|
95
|
+
function strongestEvidence(service) {
|
|
96
|
+
const evidence = service.evidence?.[0];
|
|
97
|
+
return evidence ? \`\${evidence.type}: \${evidence.value}\` : 'No evidence captured';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function App() {
|
|
101
|
+
const services = graph.services ?? [];
|
|
102
|
+
const highConfidence = services.filter((service) => service.confidence === 'high').length;
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<main className="shell">
|
|
106
|
+
<header className="topbar">
|
|
107
|
+
<div>
|
|
108
|
+
<h1>Internal Servicemap</h1>
|
|
109
|
+
<p>{graph.project?.name ?? 'This project'} uses these detected services.</p>
|
|
110
|
+
</div>
|
|
111
|
+
<span className="stamp">{new Date(graph.generatedAt).toLocaleString()}</span>
|
|
112
|
+
</header>
|
|
113
|
+
|
|
114
|
+
<section className="metrics" aria-label="Service map summary">
|
|
115
|
+
<div><strong>{services.length}</strong><span>services</span></div>
|
|
116
|
+
<div><strong>{highConfidence}</strong><span>high confidence</span></div>
|
|
117
|
+
<div><strong>{graph.project?.framework ?? 'unknown'}</strong><span>source</span></div>
|
|
118
|
+
</section>
|
|
119
|
+
|
|
120
|
+
<section className="grid">
|
|
121
|
+
{services.length ? services.map((service) => (
|
|
122
|
+
<article className="card" key={service.id}>
|
|
123
|
+
<div className="cardTop">
|
|
124
|
+
<div className="logo" aria-hidden="true">
|
|
125
|
+
{service.iconUrl ? <img src={service.iconUrl} alt="" /> : service.name.slice(0, 2).toUpperCase()}
|
|
126
|
+
</div>
|
|
127
|
+
<div className="nameBlock">
|
|
128
|
+
<strong>{service.name}</strong>
|
|
129
|
+
<span>{service.confidence} confidence</span>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
<span className="category">{service.category}</span>
|
|
133
|
+
<p>{strongestEvidence(service)}</p>
|
|
134
|
+
</article>
|
|
135
|
+
)) : <div className="empty">No external services were detected yet.</div>}
|
|
136
|
+
</section>
|
|
137
|
+
</main>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
createRoot(document.getElementById('root')).render(<App />);
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function stylesCss() {
|
|
146
|
+
return `:root {
|
|
147
|
+
color: #172033;
|
|
148
|
+
background: #f7f8fb;
|
|
149
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
* {
|
|
153
|
+
box-sizing: border-box;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
body {
|
|
157
|
+
margin: 0;
|
|
158
|
+
min-width: 320px;
|
|
159
|
+
min-height: 100vh;
|
|
160
|
+
background: #f7f8fb;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.shell {
|
|
164
|
+
max-width: 1180px;
|
|
165
|
+
margin: 0 auto;
|
|
166
|
+
padding: 40px 24px 56px;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.topbar {
|
|
170
|
+
display: flex;
|
|
171
|
+
justify-content: space-between;
|
|
172
|
+
align-items: flex-end;
|
|
173
|
+
gap: 24px;
|
|
174
|
+
margin-bottom: 24px;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
h1 {
|
|
178
|
+
margin: 0 0 8px;
|
|
179
|
+
font-size: 44px;
|
|
180
|
+
line-height: 1;
|
|
181
|
+
letter-spacing: 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
p {
|
|
185
|
+
margin: 0;
|
|
186
|
+
color: #5f6b7c;
|
|
187
|
+
line-height: 1.55;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.stamp {
|
|
191
|
+
color: #64748b;
|
|
192
|
+
font-size: 13px;
|
|
193
|
+
white-space: nowrap;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.metrics {
|
|
197
|
+
display: grid;
|
|
198
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
199
|
+
gap: 12px;
|
|
200
|
+
margin: 22px 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.metrics div,
|
|
204
|
+
.card {
|
|
205
|
+
background: #fff;
|
|
206
|
+
border: 1px solid #e3e8ef;
|
|
207
|
+
border-radius: 8px;
|
|
208
|
+
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.metrics div {
|
|
212
|
+
padding: 16px;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.metrics strong {
|
|
216
|
+
display: block;
|
|
217
|
+
font-size: 26px;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.metrics span {
|
|
221
|
+
color: #64748b;
|
|
222
|
+
font-size: 13px;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.grid {
|
|
226
|
+
display: grid;
|
|
227
|
+
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
|
228
|
+
gap: 12px;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.card {
|
|
232
|
+
min-height: 150px;
|
|
233
|
+
padding: 14px;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.cardTop {
|
|
237
|
+
display: flex;
|
|
238
|
+
align-items: center;
|
|
239
|
+
gap: 10px;
|
|
240
|
+
margin-bottom: 10px;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.logo {
|
|
244
|
+
width: 36px;
|
|
245
|
+
height: 36px;
|
|
246
|
+
border-radius: 8px;
|
|
247
|
+
background: #172033;
|
|
248
|
+
color: #fff;
|
|
249
|
+
display: grid;
|
|
250
|
+
place-items: center;
|
|
251
|
+
overflow: hidden;
|
|
252
|
+
flex: 0 0 auto;
|
|
253
|
+
font-size: 12px;
|
|
254
|
+
font-weight: 700;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.logo img {
|
|
258
|
+
max-width: 22px;
|
|
259
|
+
max-height: 22px;
|
|
260
|
+
object-fit: contain;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.nameBlock {
|
|
264
|
+
min-width: 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.nameBlock strong {
|
|
268
|
+
display: block;
|
|
269
|
+
white-space: nowrap;
|
|
270
|
+
overflow: hidden;
|
|
271
|
+
text-overflow: ellipsis;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.nameBlock span,
|
|
275
|
+
.card p {
|
|
276
|
+
color: #64748b;
|
|
277
|
+
font-size: 12px;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.category {
|
|
281
|
+
display: inline-flex;
|
|
282
|
+
align-items: center;
|
|
283
|
+
height: 22px;
|
|
284
|
+
padding: 0 8px;
|
|
285
|
+
border-radius: 999px;
|
|
286
|
+
background: #edf2ff;
|
|
287
|
+
color: #334155;
|
|
288
|
+
font-size: 12px;
|
|
289
|
+
margin-bottom: 10px;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.empty {
|
|
293
|
+
background: #fff;
|
|
294
|
+
border: 1px dashed #cbd5e1;
|
|
295
|
+
border-radius: 8px;
|
|
296
|
+
padding: 24px;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
@media (max-width: 720px) {
|
|
300
|
+
.topbar {
|
|
301
|
+
display: block;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
h1 {
|
|
305
|
+
font-size: 34px;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.metrics {
|
|
309
|
+
grid-template-columns: 1fr;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function tauriConfig(graph) {
|
|
316
|
+
return `${JSON.stringify({
|
|
317
|
+
productName: 'Internal Servicemap',
|
|
318
|
+
version: '0.1.0',
|
|
319
|
+
identifier: `com.internal.${(graph.project?.name ?? 'servicemap').replace(/[^a-z0-9]+/gi, '').toLowerCase() || 'servicemap'}`,
|
|
320
|
+
build: {
|
|
321
|
+
beforeDevCommand: 'npm run dev',
|
|
322
|
+
beforeBuildCommand: 'npm run build',
|
|
323
|
+
devUrl: 'http://localhost:5173',
|
|
324
|
+
frontendDist: '../dist',
|
|
325
|
+
},
|
|
326
|
+
app: {
|
|
327
|
+
windows: [
|
|
328
|
+
{
|
|
329
|
+
title: 'Internal Servicemap',
|
|
330
|
+
width: 1180,
|
|
331
|
+
height: 820,
|
|
332
|
+
minWidth: 860,
|
|
333
|
+
minHeight: 620,
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
},
|
|
337
|
+
bundle: {
|
|
338
|
+
active: true,
|
|
339
|
+
targets: 'all',
|
|
340
|
+
},
|
|
341
|
+
}, null, 2)}\n`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function cargoToml() {
|
|
345
|
+
return `[package]
|
|
346
|
+
name = "internal-servicemap"
|
|
347
|
+
version = "0.1.0"
|
|
348
|
+
description = "Internal Servicemap"
|
|
349
|
+
authors = ["Internal"]
|
|
350
|
+
edition = "2021"
|
|
351
|
+
|
|
352
|
+
[lib]
|
|
353
|
+
name = "internal_service_map_lib"
|
|
354
|
+
crate-type = ["staticlib", "cdylib", "rlib"]
|
|
355
|
+
|
|
356
|
+
[build-dependencies]
|
|
357
|
+
tauri-build = { version = "2", features = [] }
|
|
358
|
+
|
|
359
|
+
[dependencies]
|
|
360
|
+
tauri = { version = "2", features = [] }
|
|
361
|
+
`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function buildRs() {
|
|
365
|
+
return `fn main() {
|
|
366
|
+
tauri_build::build()
|
|
367
|
+
}
|
|
368
|
+
`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function rustLib() {
|
|
372
|
+
return `#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
373
|
+
pub fn run() {
|
|
374
|
+
tauri::Builder::default()
|
|
375
|
+
.run(tauri::generate_context!())
|
|
376
|
+
.expect("error while running tauri application");
|
|
377
|
+
}
|
|
378
|
+
`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function rustMain() {
|
|
382
|
+
return `fn main() {
|
|
383
|
+
internal_service_map_lib::run()
|
|
384
|
+
}
|
|
385
|
+
`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function main() {
|
|
389
|
+
const options = parseArgs();
|
|
390
|
+
const graph = await readJson(options.input);
|
|
391
|
+
|
|
392
|
+
await mkdir(options.output, { recursive: true });
|
|
393
|
+
await writeProjectFile(options.output, 'package.json', packageJson(graph));
|
|
394
|
+
await writeProjectFile(options.output, 'index.html', indexHtml());
|
|
395
|
+
await writeProjectFile(options.output, 'vite.config.js', viteConfig());
|
|
396
|
+
await writeProjectFile(options.output, 'src/main.jsx', mainJsx());
|
|
397
|
+
await writeProjectFile(options.output, 'src/styles.css', stylesCss());
|
|
398
|
+
await writeProjectFile(options.output, 'srcservicemap-data.json', `${JSON.stringify(graph, null, 2)}\n`);
|
|
399
|
+
await writeProjectFile(options.output, 'src-tauri/tauri.conf.json', tauriConfig(graph));
|
|
400
|
+
await writeProjectFile(options.output, 'src-tauri/Cargo.toml', cargoToml());
|
|
401
|
+
await writeProjectFile(options.output, 'src-tauri/build.rs', buildRs());
|
|
402
|
+
await writeProjectFile(options.output, 'src-tauri/src/lib.rs', rustLib());
|
|
403
|
+
await writeProjectFile(options.output, 'src-tauri/src/main.rs', rustMain());
|
|
404
|
+
|
|
405
|
+
console.log(`Wrote desktop servicemap app to ${options.output}`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
main().catch((error) => {
|
|
409
|
+
console.error(error);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const defaultInput = '.servicemap/internal-servicemap.json';
|
|
7
|
+
|
|
8
|
+
function parseArgs() {
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
const options = { root: process.cwd(), input: defaultInput };
|
|
11
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
12
|
+
if (args[index] === '--root') options.root = args[index + 1] ?? options.root;
|
|
13
|
+
if (args[index] === '--input') options.input = args[index + 1] ?? options.input;
|
|
14
|
+
}
|
|
15
|
+
options.root = resolve(options.root);
|
|
16
|
+
options.input = resolve(options.root, options.input);
|
|
17
|
+
return options;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function readJson(path) {
|
|
21
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function routeFor(root, framework) {
|
|
25
|
+
if (framework === 'next-app-router') {
|
|
26
|
+
const base = existsSync(join(root, 'src/app')) ? 'src/app' : 'app';
|
|
27
|
+
return { path: join(root, base, 'internalservicemap/page.tsx'), kind: 'next-app-router' };
|
|
28
|
+
}
|
|
29
|
+
if (framework === 'next-pages-router') {
|
|
30
|
+
const base = existsSync(join(root, 'src/pages')) ? 'src/pages' : 'pages';
|
|
31
|
+
return { path: join(root, base, 'internalservicemap.tsx'), kind: 'next-pages-router' };
|
|
32
|
+
}
|
|
33
|
+
if (framework === 'astro') {
|
|
34
|
+
return { path: join(root, 'src/pages/internalservicemap.astro'), kind: 'astro' };
|
|
35
|
+
}
|
|
36
|
+
if (framework === 'remix') {
|
|
37
|
+
return { path: join(root, 'app/routes/internalservicemap.tsx'), kind: 'remix' };
|
|
38
|
+
}
|
|
39
|
+
return { path: join(root, 'public/internalservicemap.html'), kind: 'static' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function escapeHtml(value) {
|
|
43
|
+
return String(value)
|
|
44
|
+
.replace(/&/g, '&')
|
|
45
|
+
.replace(/</g, '<')
|
|
46
|
+
.replace(/>/g, '>')
|
|
47
|
+
.replace(/"/g, '"');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function pageCss() {
|
|
51
|
+
return `
|
|
52
|
+
:root { color-scheme: light; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
53
|
+
body { margin: 0; background: #f7f8fb; color: #172033; }
|
|
54
|
+
.servicemap-shell { max-width: 1180px; margin: 0 auto; padding: 40px 24px 56px; }
|
|
55
|
+
.servicemap-header { display: flex; justify-content: space-between; gap: 24px; align-items: flex-end; margin-bottom: 24px; }
|
|
56
|
+
h1 { margin: 0 0 8px; font-size: clamp(30px, 4vw, 48px); line-height: 1; letter-spacing: 0; }
|
|
57
|
+
p { margin: 0; color: #5f6b7c; line-height: 1.55; }
|
|
58
|
+
.summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin: 22px 0; }
|
|
59
|
+
.metric, .service-card { background: #fff; border: 1px solid #e3e8ef; border-radius: 8px; box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04); }
|
|
60
|
+
.metric { padding: 16px; }
|
|
61
|
+
.metric strong { display: block; font-size: 26px; }
|
|
62
|
+
.metric span { color: #64748b; font-size: 13px; }
|
|
63
|
+
.service-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 12px; }
|
|
64
|
+
.service-card { padding: 14px; min-height: 150px; }
|
|
65
|
+
.service-top { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
|
|
66
|
+
.logo { width: 36px; height: 36px; border-radius: 8px; background: #172033; display: grid; place-items: center; overflow: hidden; flex: 0 0 auto; }
|
|
67
|
+
.logo img { max-width: 22px; max-height: 22px; object-fit: contain; }
|
|
68
|
+
.service-name { min-width: 0; }
|
|
69
|
+
.service-name strong { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
70
|
+
.service-name span, .evidence { color: #64748b; font-size: 12px; }
|
|
71
|
+
.badge { display: inline-flex; align-items: center; height: 22px; padding: 0 8px; border-radius: 999px; background: #edf2ff; color: #334155; font-size: 12px; margin-bottom: 10px; }
|
|
72
|
+
.evidence { margin-top: 8px; }
|
|
73
|
+
.empty { background: #fff; border: 1px dashed #cbd5e1; border-radius: 8px; padding: 24px; }
|
|
74
|
+
@media (max-width: 720px) { .servicemap-header { display: block; } .summary { grid-template-columns: 1fr; } }
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function htmlBody(graph) {
|
|
79
|
+
const services = graph.services ?? [];
|
|
80
|
+
const serviceCards = services.length
|
|
81
|
+
? services.map((service) => {
|
|
82
|
+
const evidence = service.evidence?.[0];
|
|
83
|
+
const evidenceText = evidence ? `${evidence.type}: ${evidence.value}` : 'No evidence captured';
|
|
84
|
+
return `
|
|
85
|
+
<article class="service-card">
|
|
86
|
+
<div class="service-top">
|
|
87
|
+
<div class="logo">${service.iconUrl ? `<img src="${escapeHtml(service.iconUrl)}" alt="">` : escapeHtml(service.name.slice(0, 2).toUpperCase())}</div>
|
|
88
|
+
<div class="service-name">
|
|
89
|
+
<strong>${escapeHtml(service.name)}</strong>
|
|
90
|
+
<span>${escapeHtml(service.confidence)} confidence</span>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<span class="badge">${escapeHtml(service.category)}</span>
|
|
94
|
+
<p class="evidence">${escapeHtml(evidenceText)}</p>
|
|
95
|
+
</article>
|
|
96
|
+
`;
|
|
97
|
+
}).join('')
|
|
98
|
+
: '<div class="empty">No external services were detected yet.</div>';
|
|
99
|
+
|
|
100
|
+
return `
|
|
101
|
+
<main class="servicemap-shell">
|
|
102
|
+
<section class="servicemap-header">
|
|
103
|
+
<div>
|
|
104
|
+
<h1>Internal Servicemap</h1>
|
|
105
|
+
<p>${escapeHtml(graph.project?.name ?? 'This project')} uses these detected services.</p>
|
|
106
|
+
</div>
|
|
107
|
+
<p>Generated ${escapeHtml(new Date(graph.generatedAt).toLocaleString())}</p>
|
|
108
|
+
</section>
|
|
109
|
+
<section class="summary" aria-label="Service map summary">
|
|
110
|
+
<div class="metric"><strong>${services.length}</strong><span>services detected</span></div>
|
|
111
|
+
<div class="metric"><strong>${graph.summary?.highConfidenceCount ?? 0}</strong><span>high confidence</span></div>
|
|
112
|
+
<div class="metric"><strong>${escapeHtml(graph.project?.framework ?? 'unknown')}</strong><span>framework</span></div>
|
|
113
|
+
</section>
|
|
114
|
+
<section class="service-grid">
|
|
115
|
+
${serviceCards}
|
|
116
|
+
</section>
|
|
117
|
+
</main>
|
|
118
|
+
`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function staticHtml(graph) {
|
|
122
|
+
return `<!doctype html>
|
|
123
|
+
<html lang="en">
|
|
124
|
+
<head>
|
|
125
|
+
<meta charset="utf-8">
|
|
126
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
127
|
+
<title>Internal Servicemap</title>
|
|
128
|
+
<style>${pageCss()}</style>
|
|
129
|
+
</head>
|
|
130
|
+
<body>
|
|
131
|
+
${htmlBody(graph)}
|
|
132
|
+
</body>
|
|
133
|
+
</html>
|
|
134
|
+
`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function tsxPage(graph) {
|
|
138
|
+
const html = htmlBody(graph);
|
|
139
|
+
return `const css = ${JSON.stringify(pageCss())};
|
|
140
|
+
const html = ${JSON.stringify(html)};
|
|
141
|
+
|
|
142
|
+
export default function InternalServiceMapPage() {
|
|
143
|
+
return (
|
|
144
|
+
<>
|
|
145
|
+
<style dangerouslySetInnerHTML={{ __html: css }} />
|
|
146
|
+
<div dangerouslySetInnerHTML={{ __html: html }} />
|
|
147
|
+
</>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function astroPage(graph) {
|
|
154
|
+
return `---
|
|
155
|
+
const css = ${JSON.stringify(pageCss())};
|
|
156
|
+
const html = ${JSON.stringify(htmlBody(graph))};
|
|
157
|
+
---
|
|
158
|
+
<html lang="en">
|
|
159
|
+
<head>
|
|
160
|
+
<meta charset="utf-8" />
|
|
161
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
162
|
+
<title>Internal Servicemap</title>
|
|
163
|
+
<style set:html={css}></style>
|
|
164
|
+
</head>
|
|
165
|
+
<body set:html={html}></body>
|
|
166
|
+
</html>
|
|
167
|
+
`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function main() {
|
|
171
|
+
const options = parseArgs();
|
|
172
|
+
const graph = await readJson(options.input);
|
|
173
|
+
const route = routeFor(options.root, graph.project?.framework);
|
|
174
|
+
const contents = route.kind === 'astro' ? astroPage(graph) : route.kind === 'static' ? staticHtml(graph) : tsxPage(graph);
|
|
175
|
+
|
|
176
|
+
await mkdir(dirname(route.path), { recursive: true });
|
|
177
|
+
await writeFile(route.path, contents);
|
|
178
|
+
console.log(`Wrote /internalservicemap page to ${route.path}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
main().catch((error) => {
|
|
182
|
+
console.error(error);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
});
|