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 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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "servicemap",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Generate an internal service map page or desktop app for a software project.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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. Inspect the project layout and current web framework.
17
- 2. 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>`.
18
- 3. Run `scripts/scan-project.mjs --root <project-root>` to create `.servicemap/internal-servicemap.json`.
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
- 4. Review the scan output for obvious false positives or missing high-confidence services.
21
- 5. 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.
22
- 6. 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`.
23
- 7. Adapt the generated surface to the project's existing design system only when the route is clear and the edits are low-risk.
24
- 8. Run the project's relevant build, lint, and focused tests.
25
- 9. 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.
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: 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
  `;