servicemap 0.1.23 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "servicemap",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
4
4
  "description": "Generate an internal service map page or desktop app for a software project.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -49,33 +49,64 @@ function escapeHtml(value) {
49
49
 
50
50
  function pageCss() {
51
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
- .access-warning { margin: 0 0 18px; padding: 12px 14px; border: 1px solid #f5c542; border-radius: 8px; background: #fff7d6; color: #614700; font-size: 14px; line-height: 1.45; }
74
- .empty { background: #fff; border: 1px dashed #cbd5e1; border-radius: 8px; padding: 24px; }
75
- @media (max-width: 720px) { .servicemap-header { display: block; } .summary { grid-template-columns: 1fr; } }
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 serviceCards = services.length
87
- ? services.map((service) => {
88
- const evidence = service.evidence?.[0];
89
- const evidenceText = evidence ? `${evidence.type}: ${evidence.value}` : 'No evidence captured';
90
- return `
91
- <article class="service-card">
92
- <div class="service-top">
93
- <div class="logo">${service.iconUrl ? `<img src="${escapeHtml(service.iconUrl)}" alt="">` : escapeHtml(service.name.slice(0, 2).toUpperCase())}</div>
94
- <div class="service-name">
95
- <strong>${escapeHtml(service.name)}</strong>
96
- <span>${escapeHtml(service.confidence)} confidence</span>
97
- </div>
98
- </div>
99
- <span class="badge">${escapeHtml(service.category)}</span>
100
- <p class="evidence">${escapeHtml(evidenceText)}</p>
101
- </article>
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
- <section class="servicemap-header">
109
- <div>
110
- <h1>Internal Servicemap</h1>
111
- <p>${escapeHtml(graph.project?.name ?? 'This project')} uses these detected services.</p>
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
- <p>Generated ${escapeHtml(new Date(graph.generatedAt).toLocaleString())}</p>
114
- </section>
115
- <section class="summary" aria-label="Service map summary">
116
- <div class="metric"><strong>${services.length}</strong><span>services detected</span></div>
117
- <div class="metric"><strong>${graph.summary?.highConfidenceCount ?? 0}</strong><span>high confidence</span></div>
118
- <div class="metric"><strong>${escapeHtml(graph.project?.framework ?? 'unknown')}</strong><span>framework</span></div>
119
- </section>
120
- ${accessNotice}
121
- <section class="service-grid">
122
- ${serviceCards}
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
  `;