servicemap 0.1.21 → 0.1.24
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'));
|
|
@@ -600,8 +693,10 @@ async function main() {
|
|
|
600
693
|
|
|
601
694
|
if (selectedMode === 'desktop') {
|
|
602
695
|
await run(process.execPath, [desktopScript, '--root', options.root]);
|
|
696
|
+
if (options.open) await openDesktopPreview(options.root);
|
|
603
697
|
} else {
|
|
604
698
|
await run(process.execPath, [pageScript, '--root', options.root]);
|
|
699
|
+
if (options.open) await openGeneratedPage(options.root, scan.project?.framework);
|
|
605
700
|
}
|
|
606
701
|
}
|
|
607
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());
|
|
@@ -49,33 +49,64 @@ function escapeHtml(value) {
|
|
|
49
49
|
|
|
50
50
|
function pageCss() {
|
|
51
51
|
return `
|
|
52
|
-
:root { color-scheme:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
.servicemap-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
.
|
|
59
|
-
.
|
|
60
|
-
.
|
|
61
|
-
.
|
|
62
|
-
.
|
|
63
|
-
.
|
|
64
|
-
.
|
|
65
|
-
.
|
|
66
|
-
.
|
|
67
|
-
.
|
|
68
|
-
.
|
|
69
|
-
.
|
|
70
|
-
.service-
|
|
71
|
-
.
|
|
72
|
-
.
|
|
73
|
-
.
|
|
74
|
-
.
|
|
75
|
-
|
|
52
|
+
:root { color-scheme: dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
53
|
+
* { box-sizing: border-box; }
|
|
54
|
+
body { margin: 0; background: #020806; color: #eff8f2; }
|
|
55
|
+
.servicemap-shell { min-height: 100vh; overflow: hidden; background: #030806; }
|
|
56
|
+
.servicemap-topbar { height: 88px; display: flex; align-items: center; justify-content: space-between; gap: 18px; padding: 0 28px; border-bottom: 1px solid rgba(255,255,255,0.08); background: #222823; }
|
|
57
|
+
.brand { display: flex; align-items: center; gap: 16px; min-width: 0; }
|
|
58
|
+
.brand-mark { width: 48px; height: 48px; border-radius: 10px; border: 1px solid #2a7958; background: #164d39; color: #7cf1b5; display: grid; place-items: center; box-shadow: 0 0 26px rgba(82,255,166,0.16); font-size: 22px; }
|
|
59
|
+
.brand h1 { margin: 0; font-size: 20px; line-height: 1; letter-spacing: 0; }
|
|
60
|
+
.brand p, .top-meta { margin: 6px 0 0; color: #8aa097; font-size: 14px; }
|
|
61
|
+
.top-actions { display: flex; align-items: center; gap: 12px; color: #8da59a; font-size: 14px; white-space: nowrap; }
|
|
62
|
+
.mode-pill { border-radius: 8px; background: #0b130f; color: #7cf1b5; padding: 8px 12px; }
|
|
63
|
+
.map { position: relative; height: calc(100vh - 88px); min-height: 760px; background: radial-gradient(circle at 50% 52%, rgba(41, 96, 69, 0.22), transparent 24%), radial-gradient(circle at 1px 1px, rgba(88, 160, 120, 0.2) 1px, transparent 1px), #020806; background-size: auto, 28px 28px, auto; }
|
|
64
|
+
.intro { position: absolute; left: 32px; top: 32px; z-index: 20; }
|
|
65
|
+
.intro strong { display: block; color: #779286; font-size: 12px; letter-spacing: 0.22em; text-transform: uppercase; }
|
|
66
|
+
.intro span { display: block; margin-top: 8px; color: #53665d; font-size: 14px; }
|
|
67
|
+
.edges { position: absolute; inset: 0; width: 100%; height: 100%; }
|
|
68
|
+
.hub { position: absolute; left: 50%; top: 50%; z-index: 20; width: 132px; height: 132px; transform: translate(-50%, -50%); border-radius: 24px; border: 1px solid #315c47; background: #09120d; display: grid; place-items: center; box-shadow: 0 0 0 9px rgba(79,183,128,0.12), 0 0 42px rgba(100,255,174,0.16); }
|
|
69
|
+
.hub-inner { width: 92px; height: 92px; border-radius: 18px; background: #fbfbf7; color: #111612; display: grid; place-items: center; font-size: 40px; box-shadow: inset 0 0 0 1px rgba(0,0,0,0.08); }
|
|
70
|
+
.service-node { position: absolute; z-index: 10; width: 250px; height: 74px; transform: translate(-50%, -50%); display: flex; align-items: center; gap: 16px; padding: 0 16px; border-radius: 8px; border: 1px solid #274036; background: rgba(16,24,19,0.95); box-shadow: 0 0 0 1px rgba(140,255,190,0.05), 0 18px 44px rgba(0,0,0,0.38); }
|
|
71
|
+
.logo { width: 48px; height: 48px; flex: 0 0 auto; border-radius: 8px; display: grid; place-items: center; overflow: hidden; color: #fff; font-weight: 700; }
|
|
72
|
+
.logo img { width: 28px; height: 28px; object-fit: contain; filter: invert(1); }
|
|
73
|
+
.service-copy { min-width: 0; }
|
|
74
|
+
.service-copy strong { display: block; color: #f2faf5; font-size: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
75
|
+
.service-copy span { display: block; margin-top: 5px; color: #7f9188; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
76
|
+
.status-dot { margin-left: auto; width: 8px; height: 8px; flex: 0 0 auto; border-radius: 999px; box-shadow: 0 0 14px currentColor; }
|
|
77
|
+
.status-high { color: #65f0ad; background: #65f0ad; }
|
|
78
|
+
.status-medium, .status-low { color: #f1c85b; background: #f1c85b; }
|
|
79
|
+
.legend { position: absolute; left: 50%; bottom: 32px; z-index: 20; transform: translateX(-50%); display: flex; align-items: center; gap: 20px; border: 1px solid rgba(255,255,255,0.1); border-radius: 999px; background: rgba(11,20,15,0.9); color: #789086; padding: 8px 20px; font-size: 12px; }
|
|
80
|
+
.legend i { display: inline-block; width: 8px; height: 8px; margin-right: 8px; border-radius: 999px; }
|
|
81
|
+
.scan-foot { position: absolute; right: 32px; bottom: 32px; z-index: 20; color: #53665d; text-align: right; font-size: 12px; line-height: 1.6; }
|
|
82
|
+
.access-warning { position: absolute; left: 32px; right: 32px; bottom: 88px; z-index: 25; max-width: 720px; border: 1px solid rgba(241,200,91,0.42); border-radius: 8px; background: rgba(31,27,12,0.92); color: #f7daa0; padding: 12px 14px; font-size: 14px; line-height: 1.45; }
|
|
83
|
+
@media (max-width: 920px) {
|
|
84
|
+
.map { min-height: 1120px; }
|
|
85
|
+
.service-node { width: 220px; }
|
|
86
|
+
.top-actions { display: none; }
|
|
87
|
+
}
|
|
76
88
|
`;
|
|
77
89
|
}
|
|
78
90
|
|
|
91
|
+
const palette = ['#ff7a00', '#f48120', '#111827', '#6d5dfc', '#4285f4', '#5e5ce6', '#e5e7eb', '#7c3aed', '#24292f', '#22c55e'];
|
|
92
|
+
const layout = [
|
|
93
|
+
{ x: 17, y: 30 }, { x: 17, y: 55 }, { x: 35, y: 28 }, { x: 35, y: 72 }, { x: 25, y: 82 },
|
|
94
|
+
{ x: 59, y: 34 }, { x: 79, y: 23 }, { x: 79, y: 48 }, { x: 80, y: 64 }, { x: 62, y: 73 },
|
|
95
|
+
{ x: 19, y: 18 }, { x: 81, y: 82 }, { x: 41, y: 17 }, { x: 61, y: 18 }, { x: 43, y: 84 }, { x: 57, y: 84 },
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
function edgePath(node) {
|
|
99
|
+
const isLeft = node.x < 50;
|
|
100
|
+
const fromX = isLeft ? node.x + 8 : node.x - 8;
|
|
101
|
+
const endX = isLeft ? 47 : 53;
|
|
102
|
+
const midX = isLeft ? Math.max(fromX + 7, 42) : Math.min(fromX - 7, 58);
|
|
103
|
+
return `M ${fromX} ${node.y} H ${midX} V 50 H ${endX}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function initials(name) {
|
|
107
|
+
return name.split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]).join('').toUpperCase();
|
|
108
|
+
}
|
|
109
|
+
|
|
79
110
|
function htmlBody(graph) {
|
|
80
111
|
const services = graph.services ?? [];
|
|
81
112
|
const access = graph.access ?? {};
|
|
@@ -83,43 +114,56 @@ function htmlBody(graph) {
|
|
|
83
114
|
const accessNotice = hasAccessRequest && access.enforced !== true
|
|
84
115
|
? '<div class="access-warning">Email access was requested, but this project does not expose a supported server-side auth guard for generated pages. Put this route behind your app auth before sharing it.</div>'
|
|
85
116
|
: '';
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
`;
|
|
103
|
-
}).join('')
|
|
104
|
-
: '<div class="empty">No external services were detected yet.</div>';
|
|
117
|
+
const nodes = services.slice(0, layout.length).map((service, index) => ({
|
|
118
|
+
...service,
|
|
119
|
+
...layout[index],
|
|
120
|
+
color: palette[index % palette.length],
|
|
121
|
+
}));
|
|
122
|
+
const edges = nodes.map((node) => `<path d="${edgePath(node)}" fill="none" stroke="rgba(103, 130, 118, 0.48)" stroke-width="0.16" vector-effect="non-scaling-stroke"></path>`).join('');
|
|
123
|
+
const serviceNodes = nodes.map((service) => `
|
|
124
|
+
<article class="service-node" style="left:${service.x}%;top:${service.y}%">
|
|
125
|
+
<div class="logo" style="background:${service.color}">${service.iconUrl ? `<img src="${escapeHtml(service.iconUrl)}" alt="">` : escapeHtml(initials(service.name))}</div>
|
|
126
|
+
<div class="service-copy">
|
|
127
|
+
<strong>${escapeHtml(service.name)}</strong>
|
|
128
|
+
<span>${escapeHtml(service.category)}</span>
|
|
129
|
+
</div>
|
|
130
|
+
<i class="status-dot status-${escapeHtml(service.confidence)}"></i>
|
|
131
|
+
</article>
|
|
132
|
+
`).join('');
|
|
105
133
|
|
|
106
134
|
return `
|
|
107
135
|
<main class="servicemap-shell">
|
|
108
|
-
<
|
|
109
|
-
<div>
|
|
110
|
-
<
|
|
111
|
-
<
|
|
136
|
+
<header class="servicemap-topbar">
|
|
137
|
+
<div class="brand">
|
|
138
|
+
<div class="brand-mark">⌘</div>
|
|
139
|
+
<div>
|
|
140
|
+
<h1>servicemap</h1>
|
|
141
|
+
<p>${escapeHtml(graph.project?.name ?? 'Project')} command center</p>
|
|
142
|
+
</div>
|
|
112
143
|
</div>
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
144
|
+
<div class="top-actions">
|
|
145
|
+
<span>${services.length} services</span>
|
|
146
|
+
<span class="mode-pill">2D</span>
|
|
147
|
+
</div>
|
|
148
|
+
</header>
|
|
149
|
+
<section class="map">
|
|
150
|
+
<div class="intro">
|
|
151
|
+
<strong>${escapeHtml(graph.project?.name ?? 'Project')} infrastructure</strong>
|
|
152
|
+
<span>Select a service to trace its dependencies</span>
|
|
153
|
+
</div>
|
|
154
|
+
<svg class="edges" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">${edges}</svg>
|
|
155
|
+
<div class="hub"><div class="hub-inner">⌘</div></div>
|
|
156
|
+
${serviceNodes || '<div class="intro"><span>No external services were detected yet.</span></div>'}
|
|
157
|
+
${accessNotice}
|
|
158
|
+
<div class="legend">
|
|
159
|
+
<span><i style="background:#4a7cff"></i>Cloud</span>
|
|
160
|
+
<span><i style="background:#8be4b2"></i>Product</span>
|
|
161
|
+
<span><i style="background:#65f0ad"></i>Active path</span>
|
|
162
|
+
</div>
|
|
163
|
+
<footer class="scan-foot">
|
|
164
|
+
<div>Generated ${escapeHtml(new Date(graph.generatedAt).toLocaleString())}</div>
|
|
165
|
+
<div>${services.length} services detected</div>
|
|
166
|
+
</footer>
|
|
123
167
|
</section>
|
|
124
168
|
</main>
|
|
125
169
|
`;
|
|
@@ -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
|
|