servicemap 0.1.23 → 0.1.25
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 +10 -0
- package/bin/servicemap.mjs +13 -1
- package/package.json +1 -1
- package/servicemap/SKILL.md +11 -9
- package/servicemap/scripts/generate-page.mjs +101 -57
package/README.md
CHANGED
|
@@ -41,6 +41,15 @@ npx servicemap skills install --agent claude
|
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
Agents with known local skill folders get the real `servicemap` skill installed.
|
|
44
|
+
Symlink installs use a stable local clone at `~/.servicemap/source`, so updates can be pulled without reinstalling every agent folder.
|
|
45
|
+
|
|
46
|
+
To update that local skill source:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx servicemap update
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Running `npx servicemap` again also refreshes the stable source before installing.
|
|
44
53
|
|
|
45
54
|
After a global install, the command is:
|
|
46
55
|
|
|
@@ -73,6 +82,7 @@ servicemap install-skill --agent grok
|
|
|
73
82
|
servicemap install-skill --agent all
|
|
74
83
|
servicemap install-skill --targets universal,claude,grok
|
|
75
84
|
servicemap skills install --agent codex
|
|
85
|
+
servicemap update
|
|
76
86
|
```
|
|
77
87
|
|
|
78
88
|
## Contents
|
package/bin/servicemap.mjs
CHANGED
|
@@ -226,6 +226,7 @@ Usage:
|
|
|
226
226
|
servicemap --root <project> [--mode auto|page|desktop] [--allow-emails <list>] [--dry-run] [--no-open]
|
|
227
227
|
servicemap install-skill [--survey] [--agent codex|claude|grok|all] [--targets <ids>] [--install-method copy|symlink]
|
|
228
228
|
servicemap skills install [--survey] [--agent codex|claude|grok|all]
|
|
229
|
+
servicemap update
|
|
229
230
|
|
|
230
231
|
Examples:
|
|
231
232
|
npx servicemap
|
|
@@ -236,6 +237,7 @@ Examples:
|
|
|
236
237
|
npx servicemap install-skill --agent all
|
|
237
238
|
npx servicemap install-skill --targets codex,claude,cursor
|
|
238
239
|
npx servicemap skills install --agent claude
|
|
240
|
+
npx servicemap update
|
|
239
241
|
servicemap --mode desktop
|
|
240
242
|
`);
|
|
241
243
|
}
|
|
@@ -593,6 +595,11 @@ async function prepareStableSource(options) {
|
|
|
593
595
|
return resolve(sourceRoot, 'servicemap');
|
|
594
596
|
}
|
|
595
597
|
|
|
598
|
+
async function updateStableSource(options) {
|
|
599
|
+
const source = await prepareStableSource(options);
|
|
600
|
+
console.log(`Servicemap source is current at ${dirname(source)}`);
|
|
601
|
+
}
|
|
602
|
+
|
|
596
603
|
async function installIntoRoot(root, source, method, options) {
|
|
597
604
|
const destination = join(root, 'servicemap');
|
|
598
605
|
if (options.dryRun) {
|
|
@@ -667,8 +674,13 @@ async function main() {
|
|
|
667
674
|
return;
|
|
668
675
|
}
|
|
669
676
|
|
|
677
|
+
if (options.command === 'update') {
|
|
678
|
+
await updateStableSource(options);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
670
682
|
if (options.command !== 'generate') {
|
|
671
|
-
throw new Error(`Unknown command "${options.command}". Use generate, install-skill, or skills install.`);
|
|
683
|
+
throw new Error(`Unknown command "${options.command}". Use generate, install-skill, update, or skills install.`);
|
|
672
684
|
}
|
|
673
685
|
|
|
674
686
|
if (!['auto', 'page', 'desktop'].includes(options.mode)) {
|
package/package.json
CHANGED
package/servicemap/SKILL.md
CHANGED
|
@@ -13,16 +13,18 @@ If no website can be found, create a desktop app fallback at `servicemap-desktop
|
|
|
13
13
|
|
|
14
14
|
## Workflow
|
|
15
15
|
|
|
16
|
-
1.
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
1. If this skill is installed from the stable source clone at `~/.servicemap/source`, run `git -C ~/.servicemap/source pull --ff-only` before using the scripts.
|
|
17
|
+
If the update fails because the network is unavailable, continue with the installed local copy and report that the auto-update did not complete.
|
|
18
|
+
2. Inspect the project layout and current web framework.
|
|
19
|
+
3. If the user gives a list of emails that may access the generated web page, pass them to the scan command with `--allow-email <email>` for each address or `--allow-emails <comma-separated-list>`.
|
|
20
|
+
4. Run `scripts/scan-project.mjs --root <project-root>` to create `.servicemap/internal-servicemap.json`.
|
|
19
21
|
Include the access flags from the previous step when relevant.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
5. Review the scan output for obvious false positives or missing high-confidence services.
|
|
23
|
+
6. If the scan detects a web framework or routeable website, run `scripts/generate-page.mjs --root <project-root>` to create the framework-specific `/internalservicemap` route.
|
|
24
|
+
7. If the scan reports `framework: static` or no website route can be safely attached, run `scripts/generate-desktop-app.mjs --root <project-root>` to create `servicemap-desktop`.
|
|
25
|
+
8. Adapt the generated surface to the project's existing design system only when the route is clear and the edits are low-risk.
|
|
26
|
+
9. Run the project's relevant build, lint, and focused tests.
|
|
27
|
+
10. Report the page or desktop app path, service count, validation run, any services marked low or medium confidence, and whether requested email access was enforced.
|
|
26
28
|
|
|
27
29
|
## Safety Rules
|
|
28
30
|
|
|
@@ -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
|
`;
|