servicemap 0.1.19 → 0.1.23
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/bin/servicemap.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import { cp, mkdir, readFile, rm, symlink } from 'node:fs/promises';
|
|
5
|
+
import net from 'node:net';
|
|
5
6
|
import { dirname, join, resolve } from 'node:path';
|
|
6
7
|
import { homedir } from 'node:os';
|
|
7
8
|
import readline from 'node:readline';
|
|
@@ -166,6 +167,7 @@ function parseArgs() {
|
|
|
166
167
|
skillsDir: null,
|
|
167
168
|
installMethod: null,
|
|
168
169
|
allowedEmails: [],
|
|
170
|
+
open: true,
|
|
169
171
|
invokedWithoutArgs: args.length === 0,
|
|
170
172
|
explicitCommand: false,
|
|
171
173
|
generationRequested: false,
|
|
@@ -207,6 +209,7 @@ function parseArgs() {
|
|
|
207
209
|
if (arg === '--survey') options.survey = true;
|
|
208
210
|
if (arg === '--yes' || arg === '-y') options.yes = true;
|
|
209
211
|
if (arg === '--dry-run') options.dryRun = true;
|
|
212
|
+
if (arg === '--no-open') options.open = false;
|
|
210
213
|
if (arg === '--help' || arg === '-h') options.help = true;
|
|
211
214
|
}
|
|
212
215
|
|
|
@@ -219,8 +222,8 @@ function help() {
|
|
|
219
222
|
|
|
220
223
|
Usage:
|
|
221
224
|
servicemap
|
|
222
|
-
servicemap generate [--root <project>] [--mode auto|page|desktop] [--allow-emails <list>] [--dry-run]
|
|
223
|
-
servicemap --root <project> [--mode auto|page|desktop] [--allow-emails <list>] [--dry-run]
|
|
225
|
+
servicemap generate [--root <project>] [--mode auto|page|desktop] [--allow-emails <list>] [--dry-run] [--no-open]
|
|
226
|
+
servicemap --root <project> [--mode auto|page|desktop] [--allow-emails <list>] [--dry-run] [--no-open]
|
|
224
227
|
servicemap install-skill [--survey] [--agent codex|claude|grok|all] [--targets <ids>] [--install-method copy|symlink]
|
|
225
228
|
servicemap skills install [--survey] [--agent codex|claude|grok|all]
|
|
226
229
|
|
|
@@ -252,6 +255,96 @@ function run(command, args, options = {}) {
|
|
|
252
255
|
});
|
|
253
256
|
}
|
|
254
257
|
|
|
258
|
+
function openUrl(url) {
|
|
259
|
+
const command = process.platform === 'darwin'
|
|
260
|
+
? 'open'
|
|
261
|
+
: process.platform === 'win32'
|
|
262
|
+
? 'cmd'
|
|
263
|
+
: 'xdg-open';
|
|
264
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
265
|
+
const child = spawn(command, args, { stdio: 'ignore', detached: true });
|
|
266
|
+
child.unref();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function findOpenPort() {
|
|
270
|
+
return new Promise((resolvePromise, reject) => {
|
|
271
|
+
const server = net.createServer();
|
|
272
|
+
server.on('error', reject);
|
|
273
|
+
server.listen(0, '127.0.0.1', () => {
|
|
274
|
+
const address = server.address();
|
|
275
|
+
const port = typeof address === 'object' && address ? address.port : 5173;
|
|
276
|
+
server.close(() => resolvePromise(port));
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function waitForPort(port, timeoutMs = 10000) {
|
|
282
|
+
const startedAt = Date.now();
|
|
283
|
+
return new Promise((resolvePromise, reject) => {
|
|
284
|
+
const tryConnect = () => {
|
|
285
|
+
const socket = net.connect({ host: '127.0.0.1', port });
|
|
286
|
+
socket.on('connect', () => {
|
|
287
|
+
socket.destroy();
|
|
288
|
+
resolvePromise();
|
|
289
|
+
});
|
|
290
|
+
socket.on('error', () => {
|
|
291
|
+
socket.destroy();
|
|
292
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
293
|
+
reject(new Error(`Timed out waiting for desktop preview on port ${port}`));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
setTimeout(tryConnect, 150);
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
tryConnect();
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function openDesktopPreview(root) {
|
|
304
|
+
const appRoot = resolve(root, 'servicemap-desktop');
|
|
305
|
+
if (!existsSync(join(appRoot, 'node_modules'))) {
|
|
306
|
+
console.log('Installing desktop preview dependencies...');
|
|
307
|
+
await run('npm', ['install'], { cwd: appRoot });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const port = await findOpenPort();
|
|
311
|
+
const child = spawn('npm', ['run', 'dev', '--', '--host', '127.0.0.1', '--port', String(port), '--strictPort'], {
|
|
312
|
+
cwd: appRoot,
|
|
313
|
+
stdio: 'ignore',
|
|
314
|
+
detached: true,
|
|
315
|
+
});
|
|
316
|
+
child.unref();
|
|
317
|
+
|
|
318
|
+
await waitForPort(port);
|
|
319
|
+
const url = `http://127.0.0.1:${port}/`;
|
|
320
|
+
openUrl(url);
|
|
321
|
+
console.log(`Opened desktop servicemap preview at ${url}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function openGeneratedPage(root, framework) {
|
|
325
|
+
if (framework === 'static') {
|
|
326
|
+
const filePath = resolve(root, 'public/internalservicemap.html');
|
|
327
|
+
openUrl(filePath);
|
|
328
|
+
console.log(`Opened generated servicemap page at ${filePath}`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const packageJson = JSON.parse(await readFile(resolve(root, 'package.json'), 'utf8').catch(() => '{}'));
|
|
333
|
+
const dependencies = {
|
|
334
|
+
...(packageJson.dependencies ?? {}),
|
|
335
|
+
...(packageJson.devDependencies ?? {}),
|
|
336
|
+
};
|
|
337
|
+
const port = dependencies.next ? 3000 : dependencies.vite ? 5173 : null;
|
|
338
|
+
if (!port) {
|
|
339
|
+
console.log('Generated /internalservicemap. Start the project dev server to view it.');
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const url = `http://127.0.0.1:${port}/internalservicemap`;
|
|
344
|
+
openUrl(url);
|
|
345
|
+
console.log(`Opened generated servicemap page at ${url}`);
|
|
346
|
+
}
|
|
347
|
+
|
|
255
348
|
async function readScan(root) {
|
|
256
349
|
const scanPath = resolve(root, '.servicemap/internal-servicemap.json');
|
|
257
350
|
return JSON.parse(await readFile(scanPath, 'utf8'));
|
|
@@ -298,6 +391,7 @@ const additionalAgentWindowSize = 8;
|
|
|
298
391
|
const dim = (value) => `\x1b[2m${value}\x1b[22m`;
|
|
299
392
|
const green = (value) => `\x1b[32m${value}\x1b[39m`;
|
|
300
393
|
const cyan = (value) => `\x1b[36m${value}\x1b[39m`;
|
|
394
|
+
const gold = (value) => `\x1b[33m${value}\x1b[39m`;
|
|
301
395
|
let stableSourceStatus = '';
|
|
302
396
|
|
|
303
397
|
function timelineStep(text) {
|
|
@@ -325,7 +419,7 @@ function renderSurvey(selected, cursor) {
|
|
|
325
419
|
timelineActive('Which agents do you want to install Servicemap to?');
|
|
326
420
|
timelineLine('Universal (.agents/skills) — always included');
|
|
327
421
|
for (const agent of universalAgents.slice(0, visibleUniversalLimit)) {
|
|
328
|
-
timelineLine(
|
|
422
|
+
timelineLine(`${green('•')} ${agent}`);
|
|
329
423
|
}
|
|
330
424
|
const hiddenUniversalCount = universalAgents.length - visibleUniversalLimit;
|
|
331
425
|
if (hiddenUniversalCount > 0) {
|
|
@@ -435,7 +529,7 @@ async function withSpinner(message, action, minimumMs = 1400) {
|
|
|
435
529
|
let dots = '';
|
|
436
530
|
const startedAt = Date.now();
|
|
437
531
|
const nextText = () => {
|
|
438
|
-
const text = `${dim('│')} ${frames[frame]} ${message}${dots}`;
|
|
532
|
+
const text = `${dim('│')} ${gold(frames[frame])} ${message}${dots}`;
|
|
439
533
|
frame = (frame + 1) % frames.length;
|
|
440
534
|
if (frame === 0) dots = dots.length >= 5 ? '' : `${dots}.`;
|
|
441
535
|
return text;
|
|
@@ -599,8 +693,10 @@ async function main() {
|
|
|
599
693
|
|
|
600
694
|
if (selectedMode === 'desktop') {
|
|
601
695
|
await run(process.execPath, [desktopScript, '--root', options.root]);
|
|
696
|
+
if (options.open) await openDesktopPreview(options.root);
|
|
602
697
|
} else {
|
|
603
698
|
await run(process.execPath, [pageScript, '--root', options.root]);
|
|
699
|
+
if (options.open) await openGeneratedPage(options.root, scan.project?.framework);
|
|
604
700
|
}
|
|
605
701
|
}
|
|
606
702
|
|
package/package.json
CHANGED
|
@@ -89,7 +89,7 @@ function indexHtml() {
|
|
|
89
89
|
function mainJsx() {
|
|
90
90
|
return `import React from 'react';
|
|
91
91
|
import { createRoot } from 'react-dom/client';
|
|
92
|
-
import graph from '
|
|
92
|
+
import graph from './servicemap-data.json';
|
|
93
93
|
import './styles.css';
|
|
94
94
|
|
|
95
95
|
function strongestEvidence(service) {
|
|
@@ -97,42 +97,111 @@ function strongestEvidence(service) {
|
|
|
97
97
|
return evidence ? \`\${evidence.type}: \${evidence.value}\` : 'No evidence captured';
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
function categoryLabel(value) {
|
|
101
|
+
return String(value ?? 'service').replace(/-/g, ' ');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function categoryClass(value) {
|
|
105
|
+
return String(value ?? 'service').replace(/[^a-z0-9]+/gi, '-').toLowerCase();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function layoutServices(services) {
|
|
109
|
+
const left = [];
|
|
110
|
+
const right = [];
|
|
111
|
+
services.forEach((service, index) => {
|
|
112
|
+
if (index % 2 === 0) left.push(service);
|
|
113
|
+
else right.push(service);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const place = (items, side) => items.map((service, index) => {
|
|
117
|
+
const count = Math.max(items.length, 1);
|
|
118
|
+
const y = 15 + ((index + 0.5) * 70) / count;
|
|
119
|
+
const x = side === 'left' ? 10 + (index % 2) * 13 : 71 + (index % 2) * 10;
|
|
120
|
+
return { service, x, y, side };
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return [...place(left, 'left'), ...place(right, 'right')];
|
|
124
|
+
}
|
|
125
|
+
|
|
100
126
|
function App() {
|
|
101
127
|
const services = graph.services ?? [];
|
|
102
128
|
const highConfidence = services.filter((service) => service.confidence === 'high').length;
|
|
129
|
+
const nodes = layoutServices(services);
|
|
103
130
|
|
|
104
131
|
return (
|
|
105
|
-
<main className="
|
|
132
|
+
<main className="appShell">
|
|
106
133
|
<header className="topbar">
|
|
107
|
-
<div>
|
|
108
|
-
<
|
|
109
|
-
<
|
|
134
|
+
<div className="brand">
|
|
135
|
+
<div className="brandIcon" aria-hidden="true">⌘</div>
|
|
136
|
+
<div>
|
|
137
|
+
<h1>service-map</h1>
|
|
138
|
+
<p>{graph.project?.name ?? 'Internal'} command center</p>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
<div className="toolbar" aria-label="Service map actions">
|
|
142
|
+
<span className="identity">{graph.project?.framework ?? 'static'}</span>
|
|
143
|
+
<button type="button">2D</button>
|
|
144
|
+
<button type="button" className="ghost">3D</button>
|
|
145
|
+
<button type="button" className="ghost">Import</button>
|
|
146
|
+
<button type="button" className="primary">+ Add service</button>
|
|
110
147
|
</div>
|
|
111
|
-
<span className="stamp">{new Date(graph.generatedAt).toLocaleString()}</span>
|
|
112
148
|
</header>
|
|
113
149
|
|
|
114
|
-
<section className="
|
|
115
|
-
<div
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
150
|
+
<section className="mapCanvas" aria-label="Detected service map">
|
|
151
|
+
<div className="sectionLabel">
|
|
152
|
+
<span>{(graph.project?.name ?? 'project').toUpperCase()} INFRASTRUCTURE</span>
|
|
153
|
+
<small>{services.length} services · {highConfidence} high confidence</small>
|
|
154
|
+
</div>
|
|
119
155
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
156
|
+
<svg className="edges" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
|
|
157
|
+
{nodes.map((node) => {
|
|
158
|
+
const startX = node.side === 'left' ? node.x + 13 : node.x;
|
|
159
|
+
const endX = 50;
|
|
160
|
+
const midX = node.side === 'left' ? 38 : 62;
|
|
161
|
+
return (
|
|
162
|
+
<path
|
|
163
|
+
key={node.service.id}
|
|
164
|
+
d={\`M \${startX} \${node.y} H \${midX} V 50 H \${endX}\`}
|
|
165
|
+
className={node.service.confidence === 'high' ? 'edge edgeHigh' : 'edge'}
|
|
166
|
+
/>
|
|
167
|
+
);
|
|
168
|
+
})}
|
|
169
|
+
</svg>
|
|
170
|
+
|
|
171
|
+
<div className="centerNode" aria-label="Project">
|
|
172
|
+
<div className="coreIcon" aria-hidden="true">⌬</div>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
{nodes.map(({ service, x, y }) => (
|
|
176
|
+
<article
|
|
177
|
+
className={\`serviceNode \${service.confidence === 'high' ? 'isHigh' : ''}\`}
|
|
178
|
+
key={service.id}
|
|
179
|
+
style={{ left: \`\${x}%\`, top: \`\${y}%\` }}
|
|
180
|
+
title={strongestEvidence(service)}
|
|
181
|
+
>
|
|
182
|
+
<div className={\`logo logo-\${categoryClass(service.category)}\`} aria-hidden="true">
|
|
183
|
+
{service.iconUrl ? <img src={service.iconUrl} alt="" /> : service.name.slice(0, 2).toUpperCase()}
|
|
184
|
+
</div>
|
|
185
|
+
<div className="nameBlock">
|
|
186
|
+
<strong>{service.name}</strong>
|
|
187
|
+
<span>{categoryLabel(service.category)}</span>
|
|
131
188
|
</div>
|
|
132
|
-
<span className=
|
|
133
|
-
<p>{strongestEvidence(service)}</p>
|
|
189
|
+
<span className={\`statusDot \${service.confidence === 'high' ? 'active' : 'pending'}\`} />
|
|
134
190
|
</article>
|
|
135
|
-
))
|
|
191
|
+
))}
|
|
192
|
+
|
|
193
|
+
{!services.length && (
|
|
194
|
+
<div className="empty">
|
|
195
|
+
<strong>No external services detected</strong>
|
|
196
|
+
<span>Run the scanner again after adding integrations.</span>
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
|
|
200
|
+
<div className="legend">
|
|
201
|
+
<span><b className="blue" /> Cloud</span>
|
|
202
|
+
<span><b className="green" /> Active path</span>
|
|
203
|
+
<span><b className="yellow" /> Lower confidence</span>
|
|
204
|
+
</div>
|
|
136
205
|
</section>
|
|
137
206
|
</main>
|
|
138
207
|
);
|
|
@@ -144,8 +213,8 @@ createRoot(document.getElementById('root')).render(<App />);
|
|
|
144
213
|
|
|
145
214
|
function stylesCss() {
|
|
146
215
|
return `:root {
|
|
147
|
-
color: #
|
|
148
|
-
background: #
|
|
216
|
+
color: #eef7f2;
|
|
217
|
+
background: #020806;
|
|
149
218
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
150
219
|
}
|
|
151
220
|
|
|
@@ -157,107 +226,244 @@ body {
|
|
|
157
226
|
margin: 0;
|
|
158
227
|
min-width: 320px;
|
|
159
228
|
min-height: 100vh;
|
|
160
|
-
background: #
|
|
229
|
+
background: #020806;
|
|
161
230
|
}
|
|
162
231
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
232
|
+
button {
|
|
233
|
+
border: 0;
|
|
234
|
+
border-radius: 8px;
|
|
235
|
+
min-height: 42px;
|
|
236
|
+
padding: 0 16px;
|
|
237
|
+
color: #e8f5ee;
|
|
238
|
+
background: #101a16;
|
|
239
|
+
font: inherit;
|
|
240
|
+
font-weight: 700;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.appShell {
|
|
244
|
+
min-height: 100vh;
|
|
245
|
+
background: #020806;
|
|
167
246
|
}
|
|
168
247
|
|
|
169
248
|
.topbar {
|
|
249
|
+
height: 86px;
|
|
250
|
+
padding: 18px 28px;
|
|
170
251
|
display: flex;
|
|
252
|
+
align-items: center;
|
|
171
253
|
justify-content: space-between;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
254
|
+
gap: 20px;
|
|
255
|
+
background: #202622;
|
|
256
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.brand {
|
|
260
|
+
display: flex;
|
|
261
|
+
align-items: center;
|
|
262
|
+
gap: 14px;
|
|
263
|
+
min-width: 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.brandIcon {
|
|
267
|
+
width: 48px;
|
|
268
|
+
height: 48px;
|
|
269
|
+
border-radius: 12px;
|
|
270
|
+
display: grid;
|
|
271
|
+
place-items: center;
|
|
272
|
+
background: #1f704f;
|
|
273
|
+
border: 1px solid #38c987;
|
|
274
|
+
box-shadow: 0 0 24px rgba(56, 201, 135, 0.25);
|
|
275
|
+
color: #c9f8df;
|
|
276
|
+
font-size: 25px;
|
|
175
277
|
}
|
|
176
278
|
|
|
177
279
|
h1 {
|
|
178
|
-
margin: 0 0
|
|
179
|
-
font-size:
|
|
280
|
+
margin: 0 0 3px;
|
|
281
|
+
font-size: 22px;
|
|
180
282
|
line-height: 1;
|
|
181
283
|
letter-spacing: 0;
|
|
182
284
|
}
|
|
183
285
|
|
|
184
286
|
p {
|
|
185
287
|
margin: 0;
|
|
186
|
-
color: #5f6b7c;
|
|
187
|
-
line-height: 1.55;
|
|
188
288
|
}
|
|
189
289
|
|
|
190
|
-
.
|
|
191
|
-
|
|
290
|
+
.brand p,
|
|
291
|
+
.nameBlock span,
|
|
292
|
+
.sectionLabel,
|
|
293
|
+
.legend {
|
|
294
|
+
color: rgba(238, 247, 242, 0.54);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.toolbar {
|
|
298
|
+
display: flex;
|
|
299
|
+
align-items: center;
|
|
300
|
+
gap: 10px;
|
|
301
|
+
min-width: 0;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.identity {
|
|
305
|
+
color: #93c8ac;
|
|
192
306
|
font-size: 13px;
|
|
307
|
+
font-weight: 800;
|
|
308
|
+
margin-right: 10px;
|
|
193
309
|
white-space: nowrap;
|
|
194
310
|
}
|
|
195
311
|
|
|
196
|
-
.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
gap: 12px;
|
|
200
|
-
margin: 22px 0;
|
|
312
|
+
.toolbar .primary {
|
|
313
|
+
background: #247b58;
|
|
314
|
+
box-shadow: inset 0 0 0 1px rgba(104, 255, 179, 0.25);
|
|
201
315
|
}
|
|
202
316
|
|
|
203
|
-
.
|
|
204
|
-
.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
317
|
+
.toolbar .ghost {
|
|
318
|
+
color: rgba(238, 247, 242, 0.72);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.mapCanvas {
|
|
322
|
+
position: relative;
|
|
323
|
+
min-height: calc(100vh - 86px);
|
|
324
|
+
overflow: hidden;
|
|
325
|
+
background:
|
|
326
|
+
radial-gradient(circle at center, rgba(37, 128, 88, 0.16), transparent 34%),
|
|
327
|
+
radial-gradient(circle at 1px 1px, rgba(115, 255, 183, 0.16) 1px, transparent 1px),
|
|
328
|
+
#020806;
|
|
329
|
+
background-size: auto, 28px 28px, auto;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.sectionLabel {
|
|
333
|
+
position: absolute;
|
|
334
|
+
top: 28px;
|
|
335
|
+
left: 32px;
|
|
336
|
+
right: 32px;
|
|
337
|
+
display: flex;
|
|
338
|
+
justify-content: space-between;
|
|
339
|
+
gap: 18px;
|
|
340
|
+
font-size: 13px;
|
|
341
|
+
font-weight: 800;
|
|
342
|
+
letter-spacing: 0.16em;
|
|
343
|
+
text-transform: uppercase;
|
|
209
344
|
}
|
|
210
345
|
|
|
211
|
-
.
|
|
212
|
-
|
|
346
|
+
.sectionLabel small {
|
|
347
|
+
letter-spacing: 0.08em;
|
|
213
348
|
}
|
|
214
349
|
|
|
215
|
-
.
|
|
216
|
-
|
|
217
|
-
|
|
350
|
+
.edges {
|
|
351
|
+
position: absolute;
|
|
352
|
+
inset: 0;
|
|
353
|
+
width: 100%;
|
|
354
|
+
height: 100%;
|
|
355
|
+
pointer-events: none;
|
|
218
356
|
}
|
|
219
357
|
|
|
220
|
-
.
|
|
221
|
-
|
|
222
|
-
|
|
358
|
+
.edge {
|
|
359
|
+
fill: none;
|
|
360
|
+
stroke: rgba(121, 158, 140, 0.34);
|
|
361
|
+
stroke-width: 0.12;
|
|
223
362
|
}
|
|
224
363
|
|
|
225
|
-
.
|
|
364
|
+
.edgeHigh {
|
|
365
|
+
stroke: rgba(115, 255, 183, 0.5);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.centerNode {
|
|
369
|
+
position: absolute;
|
|
370
|
+
left: 50%;
|
|
371
|
+
top: 50%;
|
|
372
|
+
width: 112px;
|
|
373
|
+
height: 112px;
|
|
374
|
+
transform: translate(-50%, -50%);
|
|
226
375
|
display: grid;
|
|
227
|
-
|
|
228
|
-
|
|
376
|
+
place-items: center;
|
|
377
|
+
border-radius: 26px;
|
|
378
|
+
background: rgba(15, 31, 24, 0.88);
|
|
379
|
+
border: 1px solid rgba(114, 255, 183, 0.28);
|
|
380
|
+
box-shadow: 0 0 0 9px rgba(56, 201, 135, 0.08), 0 0 45px rgba(56, 201, 135, 0.18);
|
|
229
381
|
}
|
|
230
382
|
|
|
231
|
-
.
|
|
232
|
-
|
|
233
|
-
|
|
383
|
+
.coreIcon {
|
|
384
|
+
width: 78px;
|
|
385
|
+
height: 78px;
|
|
386
|
+
display: grid;
|
|
387
|
+
place-items: center;
|
|
388
|
+
border-radius: 20px;
|
|
389
|
+
background: #f7fbf7;
|
|
390
|
+
color: #101513;
|
|
391
|
+
font-size: 38px;
|
|
234
392
|
}
|
|
235
393
|
|
|
236
|
-
.
|
|
237
|
-
|
|
394
|
+
.serviceNode {
|
|
395
|
+
position: absolute;
|
|
396
|
+
width: clamp(190px, 16vw, 260px);
|
|
397
|
+
min-height: 70px;
|
|
398
|
+
transform: translateY(-50%);
|
|
399
|
+
display: grid;
|
|
400
|
+
grid-template-columns: 48px minmax(0, 1fr) 12px;
|
|
238
401
|
align-items: center;
|
|
239
|
-
gap:
|
|
240
|
-
|
|
402
|
+
gap: 12px;
|
|
403
|
+
padding: 12px;
|
|
404
|
+
border-radius: 14px;
|
|
405
|
+
background: linear-gradient(180deg, rgba(22, 33, 27, 0.98), rgba(12, 20, 16, 0.96));
|
|
406
|
+
border: 1px solid rgba(158, 202, 176, 0.24);
|
|
407
|
+
box-shadow: 0 12px 26px rgba(0, 0, 0, 0.34), inset 0 0 0 1px rgba(255, 255, 255, 0.03);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.serviceNode.isHigh {
|
|
411
|
+
box-shadow: 0 12px 26px rgba(0, 0, 0, 0.34), 0 0 24px rgba(56, 201, 135, 0.08);
|
|
241
412
|
}
|
|
242
413
|
|
|
243
414
|
.logo {
|
|
244
|
-
width:
|
|
245
|
-
height:
|
|
246
|
-
border-radius:
|
|
247
|
-
background: #
|
|
415
|
+
width: 48px;
|
|
416
|
+
height: 48px;
|
|
417
|
+
border-radius: 12px;
|
|
418
|
+
background: #24302b;
|
|
248
419
|
color: #fff;
|
|
249
420
|
display: grid;
|
|
250
421
|
place-items: center;
|
|
251
422
|
overflow: hidden;
|
|
252
423
|
flex: 0 0 auto;
|
|
253
424
|
font-size: 12px;
|
|
254
|
-
font-weight:
|
|
425
|
+
font-weight: 800;
|
|
255
426
|
}
|
|
256
427
|
|
|
257
428
|
.logo img {
|
|
258
|
-
|
|
259
|
-
|
|
429
|
+
width: 100%;
|
|
430
|
+
height: 100%;
|
|
260
431
|
object-fit: contain;
|
|
432
|
+
padding: 7px;
|
|
433
|
+
filter: brightness(0) invert(1);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.logo-cloud {
|
|
437
|
+
background: #2d72f6;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.logo-developer-tools {
|
|
441
|
+
background: #202833;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.logo-billing {
|
|
445
|
+
background: #635bff;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.logo-email,
|
|
449
|
+
.logo-collaboration {
|
|
450
|
+
background: #f45d48;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.logo-ai {
|
|
454
|
+
background: #1f7a55;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.logo-monitoring {
|
|
458
|
+
background: #6b4bd8;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.logo-auth {
|
|
462
|
+
background: #28a37a;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.logo-database {
|
|
466
|
+
background: #f57c00;
|
|
261
467
|
}
|
|
262
468
|
|
|
263
469
|
.nameBlock {
|
|
@@ -266,47 +472,108 @@ p {
|
|
|
266
472
|
|
|
267
473
|
.nameBlock strong {
|
|
268
474
|
display: block;
|
|
475
|
+
overflow: hidden;
|
|
476
|
+
text-overflow: ellipsis;
|
|
269
477
|
white-space: nowrap;
|
|
478
|
+
font-size: 15px;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.nameBlock span {
|
|
482
|
+
display: block;
|
|
483
|
+
margin-top: 4px;
|
|
270
484
|
overflow: hidden;
|
|
271
485
|
text-overflow: ellipsis;
|
|
486
|
+
white-space: nowrap;
|
|
487
|
+
font-size: 12px;
|
|
488
|
+
text-transform: capitalize;
|
|
272
489
|
}
|
|
273
490
|
|
|
274
|
-
.
|
|
275
|
-
|
|
276
|
-
|
|
491
|
+
.statusDot {
|
|
492
|
+
width: 8px;
|
|
493
|
+
height: 8px;
|
|
494
|
+
border-radius: 999px;
|
|
495
|
+
background: #eac94f;
|
|
496
|
+
box-shadow: 0 0 12px rgba(234, 201, 79, 0.9);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.statusDot.active {
|
|
500
|
+
background: #69f6b1;
|
|
501
|
+
box-shadow: 0 0 12px rgba(105, 246, 177, 0.9);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.empty {
|
|
505
|
+
position: absolute;
|
|
506
|
+
left: 50%;
|
|
507
|
+
top: 50%;
|
|
508
|
+
transform: translate(-50%, 90px);
|
|
509
|
+
display: grid;
|
|
510
|
+
gap: 6px;
|
|
511
|
+
text-align: center;
|
|
512
|
+
color: rgba(238, 247, 242, 0.74);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.legend {
|
|
516
|
+
position: absolute;
|
|
517
|
+
left: 50%;
|
|
518
|
+
bottom: 28px;
|
|
519
|
+
transform: translateX(-50%);
|
|
520
|
+
display: flex;
|
|
521
|
+
gap: 20px;
|
|
522
|
+
padding: 8px 16px;
|
|
523
|
+
border-radius: 999px;
|
|
524
|
+
background: rgba(8, 15, 12, 0.82);
|
|
525
|
+
border: 1px solid rgba(158, 202, 176, 0.18);
|
|
277
526
|
font-size: 12px;
|
|
527
|
+
font-weight: 800;
|
|
278
528
|
}
|
|
279
529
|
|
|
280
|
-
.
|
|
530
|
+
.legend span {
|
|
281
531
|
display: inline-flex;
|
|
282
532
|
align-items: center;
|
|
283
|
-
|
|
284
|
-
|
|
533
|
+
gap: 7px;
|
|
534
|
+
white-space: nowrap;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.legend b {
|
|
538
|
+
width: 8px;
|
|
539
|
+
height: 8px;
|
|
285
540
|
border-radius: 999px;
|
|
286
|
-
background: #edf2ff;
|
|
287
|
-
color: #334155;
|
|
288
|
-
font-size: 12px;
|
|
289
|
-
margin-bottom: 10px;
|
|
290
541
|
}
|
|
291
542
|
|
|
292
|
-
.
|
|
293
|
-
background: #
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
543
|
+
.legend .blue {
|
|
544
|
+
background: #3d8cff;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.legend .green {
|
|
548
|
+
background: #69f6b1;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.legend .yellow {
|
|
552
|
+
background: #eac94f;
|
|
297
553
|
}
|
|
298
554
|
|
|
299
|
-
@media (max-width:
|
|
555
|
+
@media (max-width: 880px) {
|
|
300
556
|
.topbar {
|
|
301
|
-
|
|
557
|
+
height: auto;
|
|
558
|
+
align-items: flex-start;
|
|
559
|
+
flex-direction: column;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.toolbar {
|
|
563
|
+
flex-wrap: wrap;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.mapCanvas {
|
|
567
|
+
min-height: 980px;
|
|
302
568
|
}
|
|
303
569
|
|
|
304
|
-
|
|
305
|
-
|
|
570
|
+
.serviceNode {
|
|
571
|
+
width: 210px;
|
|
306
572
|
}
|
|
307
573
|
|
|
308
|
-
.
|
|
309
|
-
|
|
574
|
+
.sectionLabel {
|
|
575
|
+
align-items: flex-start;
|
|
576
|
+
flex-direction: column;
|
|
310
577
|
}
|
|
311
578
|
}
|
|
312
579
|
`;
|
|
@@ -395,7 +662,7 @@ async function main() {
|
|
|
395
662
|
await writeProjectFile(options.output, 'vite.config.js', viteConfig());
|
|
396
663
|
await writeProjectFile(options.output, 'src/main.jsx', mainJsx());
|
|
397
664
|
await writeProjectFile(options.output, 'src/styles.css', stylesCss());
|
|
398
|
-
await writeProjectFile(options.output, '
|
|
665
|
+
await writeProjectFile(options.output, 'src/servicemap-data.json', `${JSON.stringify(graph, null, 2)}\n`);
|
|
399
666
|
await writeProjectFile(options.output, 'src-tauri/tauri.conf.json', tauriConfig(graph));
|
|
400
667
|
await writeProjectFile(options.output, 'src-tauri/Cargo.toml', cargoToml());
|
|
401
668
|
await writeProjectFile(options.output, 'src-tauri/build.rs', buildRs());
|
|
@@ -10,6 +10,7 @@ const logoDatabasePath = resolve(skillRoot, 'assets/service-logo-database.json')
|
|
|
10
10
|
const defaultOutput = '.servicemap/internal-servicemap.json';
|
|
11
11
|
|
|
12
12
|
const ignoredDirs = new Set([
|
|
13
|
+
'.servicemap',
|
|
13
14
|
'.git',
|
|
14
15
|
'.next',
|
|
15
16
|
'.nuxt',
|
|
@@ -17,12 +18,14 @@ const ignoredDirs = new Set([
|
|
|
17
18
|
'build',
|
|
18
19
|
'coverage',
|
|
19
20
|
'dist',
|
|
21
|
+
'graphify-out',
|
|
20
22
|
'node_modules',
|
|
23
|
+
'servicemap-desktop',
|
|
21
24
|
'target',
|
|
22
25
|
'vendor',
|
|
23
26
|
]);
|
|
24
27
|
|
|
25
|
-
const sourceExtensions = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.astro', '.vue', '.svelte', '.py', '.rb', '.go', '.rs', '.php', '.java']);
|
|
28
|
+
const sourceExtensions = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.astro', '.vue', '.svelte', '.py', '.rb', '.go', '.rs', '.php', '.java', '.swift']);
|
|
26
29
|
const configFileNames = new Set([
|
|
27
30
|
'Dockerfile',
|
|
28
31
|
'docker-compose.yml',
|
|
@@ -187,7 +190,7 @@ function buildGeneratedRules(database) {
|
|
|
187
190
|
iconUrl: entry.iconUrl,
|
|
188
191
|
packages: [entry.key, entry.name.toLowerCase().replace(/\s+/g, ''), ...entry.aliases.map((alias) => alias.toLowerCase())].filter(Boolean),
|
|
189
192
|
envPrefixes: [entry.key.replace(/[^a-z0-9]/gi, '_').toUpperCase()],
|
|
190
|
-
domains: [
|
|
193
|
+
domains: [],
|
|
191
194
|
}));
|
|
192
195
|
}
|
|
193
196
|
|