servicemap 0.1.4 → 0.1.5
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 +6 -4
- package/bin/servicemap.mjs +53 -31
- package/package.json +2 -2
- package/servicemap/SKILL.md +12 -7
- package/servicemap/scripts/generate-page.mjs +37 -0
- package/servicemap/scripts/scan-project.mjs +15 -1
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ 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
|
-
Other selected AI tools get
|
|
44
|
+
Other selected AI tools get setup notes under `~/.servicemap/model-guides`.
|
|
45
45
|
|
|
46
46
|
After a global install, the command is:
|
|
47
47
|
|
|
@@ -61,6 +61,8 @@ CLI options:
|
|
|
61
61
|
servicemap
|
|
62
62
|
servicemap generate --root <project-path>
|
|
63
63
|
servicemap --root <project-path>
|
|
64
|
+
servicemap generate --root <project-path> --allow-emails founder@example.com,ops@example.com
|
|
65
|
+
servicemap generate --root <project-path> --allow-email founder@example.com --allow-email ops@example.com
|
|
64
66
|
servicemap --mode page
|
|
65
67
|
servicemap --mode desktop
|
|
66
68
|
servicemap --dry-run
|
|
@@ -70,7 +72,7 @@ servicemap install-skill --agent codex
|
|
|
70
72
|
servicemap install-skill --agent claude
|
|
71
73
|
servicemap install-skill --agent grok
|
|
72
74
|
servicemap install-skill --agent all
|
|
73
|
-
servicemap install-skill --targets codex,
|
|
75
|
+
servicemap install-skill --targets codex,gemini,cursor
|
|
74
76
|
servicemap skills install --agent codex
|
|
75
77
|
```
|
|
76
78
|
|
|
@@ -86,11 +88,11 @@ servicemap skills install --agent codex
|
|
|
86
88
|
## Validation
|
|
87
89
|
|
|
88
90
|
```bash
|
|
89
|
-
node bin/servicemap.mjs generate --root demo-project --dry-run
|
|
91
|
+
node bin/servicemap.mjs generate --root demo-project --allow-emails founder@example.com,ops@example.com --dry-run
|
|
90
92
|
node bin/servicemap.mjs --root no-website-demo --dry-run
|
|
91
93
|
node bin/servicemap.mjs --dry-run
|
|
92
94
|
node bin/servicemap.mjs install-skill --agent all --dry-run
|
|
93
|
-
node bin/servicemap.mjs install-skill --targets codex,
|
|
95
|
+
node bin/servicemap.mjs install-skill --targets codex,gemini,cursor --dry-run
|
|
94
96
|
node bin/servicemap.mjs skills install --agent claude --dry-run
|
|
95
97
|
python3 /Users/dougie/.codex/skills/.system/skill-creator/scripts/quick_validate.py servicemap
|
|
96
98
|
```
|
package/bin/servicemap.mjs
CHANGED
|
@@ -13,21 +13,10 @@ const pageScript = resolve(packageRoot, 'servicemap/scripts/generate-page.mjs');
|
|
|
13
13
|
const desktopScript = resolve(packageRoot, 'servicemap/scripts/generate-desktop-app.mjs');
|
|
14
14
|
|
|
15
15
|
const targetCatalog = [
|
|
16
|
-
{ id: 'codex', label: 'Codex
|
|
17
|
-
{ id: 'claude', label: 'Claude Code', kind: 'skill', root: () => resolve(homedir(), '.claude/skills'), recommended: true },
|
|
18
|
-
{ id: 'grok', label: 'Grok', kind: 'skill', root: () => resolve(homedir(), '.grok/skills'), recommended: true },
|
|
19
|
-
{ id: '
|
|
20
|
-
{ id: 'gpt-6-astra', label: 'GPT-6 Astra', kind: 'guide' },
|
|
21
|
-
{ id: 'gpt-6-sol', label: 'GPT-6 Sol', kind: 'guide' },
|
|
22
|
-
{ id: 'gpt-6-luna', label: 'GPT-6 Luna', kind: 'guide' },
|
|
23
|
-
{ id: 'gpt-5.6-sol', label: 'GPT-5.6 Sol', kind: 'guide' },
|
|
24
|
-
{ id: 'gpt-5.6-terra', label: 'GPT-5.6 Terra', kind: 'guide' },
|
|
25
|
-
{ id: 'gpt-5.6-luna', label: 'GPT-5.6 Luna', kind: 'guide' },
|
|
26
|
-
{ id: 'claude-opus', label: 'Claude Opus', kind: 'guide' },
|
|
27
|
-
{ id: 'claude-sonnet', label: 'Claude Sonnet', kind: 'guide' },
|
|
28
|
-
{ id: 'claude-haiku', label: 'Claude Haiku', kind: 'guide' },
|
|
29
|
-
{ id: 'gemini-pro', label: 'Gemini Pro', kind: 'guide' },
|
|
30
|
-
{ id: 'gemini-flash', label: 'Gemini Flash', kind: 'guide' },
|
|
16
|
+
{ id: 'codex', label: 'OpenAI (Codex, ChatGPT, GPT models)', kind: 'skill', root: () => resolve(homedir(), '.agents/skills'), recommended: true },
|
|
17
|
+
{ id: 'claude', label: 'Anthropic (Claude Code, Claude models)', kind: 'skill', root: () => resolve(homedir(), '.claude/skills'), recommended: true },
|
|
18
|
+
{ id: 'grok', label: 'xAI (Grok)', kind: 'skill', root: () => resolve(homedir(), '.grok/skills'), recommended: true },
|
|
19
|
+
{ id: 'gemini', label: 'Google (Gemini, Vertex AI)', kind: 'guide' },
|
|
31
20
|
{ id: 'cursor', label: 'Cursor', kind: 'guide' },
|
|
32
21
|
{ id: 'windsurf', label: 'Windsurf', kind: 'guide' },
|
|
33
22
|
{ id: 'copilot', label: 'GitHub Copilot', kind: 'guide' },
|
|
@@ -42,19 +31,14 @@ const targetCatalog = [
|
|
|
42
31
|
{ id: 'bolt', label: 'Bolt', kind: 'guide' },
|
|
43
32
|
{ id: 'v0', label: 'v0', kind: 'guide' },
|
|
44
33
|
{ id: 'perplexity', label: 'Perplexity', kind: 'guide' },
|
|
45
|
-
{ id: 'mistral', label: 'Mistral
|
|
34
|
+
{ id: 'mistral', label: 'Mistral', kind: 'guide' },
|
|
46
35
|
{ id: 'deepseek', label: 'DeepSeek', kind: 'guide' },
|
|
47
36
|
{ id: 'qwen', label: 'Qwen', kind: 'guide' },
|
|
48
|
-
{ id: '
|
|
49
|
-
{ id: '
|
|
50
|
-
{ id: 'lm-studio', label: 'LM Studio', kind: 'guide' },
|
|
37
|
+
{ id: 'meta', label: 'Meta (Llama)', kind: 'guide' },
|
|
38
|
+
{ id: 'local-models', label: 'Local models (Ollama, LM Studio)', kind: 'guide' },
|
|
51
39
|
{ id: 'openrouter', label: 'OpenRouter', kind: 'guide' },
|
|
52
|
-
{ id: 'anthropic-api', label: 'Anthropic API apps', kind: 'guide' },
|
|
53
|
-
{ id: 'openai-api', label: 'OpenAI API apps', kind: 'guide' },
|
|
54
|
-
{ id: 'gemini-api', label: 'Gemini API apps', kind: 'guide' },
|
|
55
40
|
{ id: 'bedrock', label: 'Amazon Bedrock', kind: 'guide' },
|
|
56
41
|
{ id: 'azure-openai', label: 'Azure OpenAI', kind: 'guide' },
|
|
57
|
-
{ id: 'vertex-ai', label: 'Vertex AI', kind: 'guide' },
|
|
58
42
|
{ id: 'poe', label: 'Poe', kind: 'guide' },
|
|
59
43
|
{ id: 'you-com', label: 'You.com', kind: 'guide' },
|
|
60
44
|
{ id: 'phind', label: 'Phind', kind: 'guide' },
|
|
@@ -65,6 +49,32 @@ const targetCatalog = [
|
|
|
65
49
|
{ id: 'custom', label: 'Other custom model or agent', kind: 'guide' },
|
|
66
50
|
];
|
|
67
51
|
|
|
52
|
+
const targetAliases = new Map([
|
|
53
|
+
['agents', 'codex'],
|
|
54
|
+
['chatgpt', 'codex'],
|
|
55
|
+
['openai', 'codex'],
|
|
56
|
+
['openai-api', 'codex'],
|
|
57
|
+
['gpt-6-astra', 'codex'],
|
|
58
|
+
['gpt-6-sol', 'codex'],
|
|
59
|
+
['gpt-6-luna', 'codex'],
|
|
60
|
+
['gpt-5.6-sol', 'codex'],
|
|
61
|
+
['gpt-5.6-terra', 'codex'],
|
|
62
|
+
['gpt-5.6-luna', 'codex'],
|
|
63
|
+
['anthropic', 'claude'],
|
|
64
|
+
['anthropic-api', 'claude'],
|
|
65
|
+
['claude-opus', 'claude'],
|
|
66
|
+
['claude-sonnet', 'claude'],
|
|
67
|
+
['claude-haiku', 'claude'],
|
|
68
|
+
['google', 'gemini'],
|
|
69
|
+
['gemini-pro', 'gemini'],
|
|
70
|
+
['gemini-flash', 'gemini'],
|
|
71
|
+
['gemini-api', 'gemini'],
|
|
72
|
+
['vertex-ai', 'gemini'],
|
|
73
|
+
['llama', 'meta'],
|
|
74
|
+
['ollama', 'local-models'],
|
|
75
|
+
['lm-studio', 'local-models'],
|
|
76
|
+
]);
|
|
77
|
+
|
|
68
78
|
function parseArgs() {
|
|
69
79
|
const args = process.argv.slice(2);
|
|
70
80
|
const options = {
|
|
@@ -75,6 +85,7 @@ function parseArgs() {
|
|
|
75
85
|
agent: 'codex',
|
|
76
86
|
targets: null,
|
|
77
87
|
skillsDir: null,
|
|
88
|
+
allowedEmails: [],
|
|
78
89
|
invokedWithoutArgs: args.length === 0,
|
|
79
90
|
explicitCommand: false,
|
|
80
91
|
generationRequested: false,
|
|
@@ -102,6 +113,14 @@ function parseArgs() {
|
|
|
102
113
|
}
|
|
103
114
|
if (arg === '--agent') options.agent = args[++index] ?? options.agent;
|
|
104
115
|
if (arg === '--targets') options.targets = (args[++index] ?? '').split(',').map((target) => target.trim()).filter(Boolean);
|
|
116
|
+
if (arg === '--allow-email') {
|
|
117
|
+
options.allowedEmails.push(args[++index] ?? '');
|
|
118
|
+
options.generationRequested = true;
|
|
119
|
+
}
|
|
120
|
+
if (arg === '--allow-emails') {
|
|
121
|
+
options.allowedEmails.push(...(args[++index] ?? '').split(','));
|
|
122
|
+
options.generationRequested = true;
|
|
123
|
+
}
|
|
105
124
|
if (arg === '--skills-dir') options.skillsDir = resolve(args[++index] ?? '.');
|
|
106
125
|
if (arg === '--user') options.user = true;
|
|
107
126
|
if (arg === '--survey') options.survey = true;
|
|
@@ -119,14 +138,14 @@ function help() {
|
|
|
119
138
|
|
|
120
139
|
Usage:
|
|
121
140
|
servicemap
|
|
122
|
-
servicemap generate [--root <project>] [--mode auto|page|desktop] [--dry-run]
|
|
123
|
-
servicemap --root <project> [--mode auto|page|desktop] [--dry-run]
|
|
141
|
+
servicemap generate [--root <project>] [--mode auto|page|desktop] [--allow-emails <list>] [--dry-run]
|
|
142
|
+
servicemap --root <project> [--mode auto|page|desktop] [--allow-emails <list>] [--dry-run]
|
|
124
143
|
servicemap install-skill [--survey] [--agent codex|claude|grok|all] [--targets <ids>]
|
|
125
144
|
servicemap skills install [--survey] [--agent codex|claude|grok|all]
|
|
126
145
|
|
|
127
146
|
Examples:
|
|
128
147
|
npx servicemap
|
|
129
|
-
npx servicemap generate --root ../my-app
|
|
148
|
+
npx servicemap generate --root ../my-app --allow-emails founder@example.com,ops@example.com
|
|
130
149
|
npx servicemap -- --root ../my-app
|
|
131
150
|
npx servicemap install-skill
|
|
132
151
|
npx servicemap install-skill --survey
|
|
@@ -160,7 +179,7 @@ async function readScan(root) {
|
|
|
160
179
|
function targetsByIds(ids) {
|
|
161
180
|
const byId = new Map(targetCatalog.map((target) => [target.id, target]));
|
|
162
181
|
return ids.map((id) => {
|
|
163
|
-
const target = byId.get(id);
|
|
182
|
+
const target = byId.get(targetAliases.get(id) ?? id);
|
|
164
183
|
if (!target) {
|
|
165
184
|
throw new Error(`Unknown target "${id}". Use --survey to choose from the list.`);
|
|
166
185
|
}
|
|
@@ -171,7 +190,6 @@ function targetsByIds(ids) {
|
|
|
171
190
|
function targetsForOptions(options) {
|
|
172
191
|
if (options.targets?.length) return targetsByIds(options.targets);
|
|
173
192
|
if (options.agent === 'all') return targetCatalog.filter((target) => target.kind === 'skill');
|
|
174
|
-
if (options.agent === 'agents') return targetsByIds(['codex']);
|
|
175
193
|
return targetsByIds([options.agent]);
|
|
176
194
|
}
|
|
177
195
|
|
|
@@ -193,7 +211,7 @@ function renderSurvey(selected, cursor) {
|
|
|
193
211
|
targetCatalog.forEach((target, index) => {
|
|
194
212
|
const pointer = index === cursor ? '>' : ' ';
|
|
195
213
|
const checked = selected.has(target.id) ? 'x' : ' ';
|
|
196
|
-
const detail = target.kind === 'skill' ? 'skill folder' : '
|
|
214
|
+
const detail = target.kind === 'skill' ? 'skill folder' : 'setup notes';
|
|
197
215
|
console.log(`${pointer} [${checked}] ${target.label} (${detail})`);
|
|
198
216
|
});
|
|
199
217
|
}
|
|
@@ -242,7 +260,7 @@ function runTargetSurvey() {
|
|
|
242
260
|
function guideText(target) {
|
|
243
261
|
return `# Servicemap for ${target.label}
|
|
244
262
|
|
|
245
|
-
|
|
263
|
+
These setup notes help ${target.label} use Servicemap when you want it to create a service map for a software project.
|
|
246
264
|
|
|
247
265
|
Invoke: /servicemap
|
|
248
266
|
|
|
@@ -336,7 +354,11 @@ async function main() {
|
|
|
336
354
|
throw new Error(`Unknown mode "${options.mode}". Use auto, page, or desktop.`);
|
|
337
355
|
}
|
|
338
356
|
|
|
339
|
-
|
|
357
|
+
const scanArgs = [scanScript, '--root', options.root];
|
|
358
|
+
for (const email of options.allowedEmails) {
|
|
359
|
+
scanArgs.push('--allow-email', email);
|
|
360
|
+
}
|
|
361
|
+
await run(process.execPath, scanArgs);
|
|
340
362
|
const scan = await readScan(options.root);
|
|
341
363
|
const selectedMode = options.mode === 'auto'
|
|
342
364
|
? scan.project?.framework === 'static' ? 'desktop' : 'page'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "servicemap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Generate an internal service map page or desktop app for a software project.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"README.md"
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
|
-
"test": "node bin/servicemap.mjs generate --root demo-project --dry-run && node bin/servicemap.mjs --root no-website-demo --dry-run && node bin/servicemap.mjs --dry-run && HOME=\"$(mktemp -d)\" node bin/servicemap.mjs && node bin/servicemap.mjs install-skill --agent all --dry-run && node bin/servicemap.mjs install-skill --targets codex,
|
|
15
|
+
"test": "node bin/servicemap.mjs generate --root demo-project --allow-emails founder@example.com,ops@example.com --dry-run && node bin/servicemap.mjs --root no-website-demo --dry-run && node bin/servicemap.mjs --dry-run && HOME=\"$(mktemp -d)\" node bin/servicemap.mjs && node bin/servicemap.mjs install-skill --agent all --dry-run && node bin/servicemap.mjs install-skill --targets codex,gemini,cursor --dry-run && node bin/servicemap.mjs install-skill --targets chatgpt,gpt-6-astra,gemini-flash --dry-run && node bin/servicemap.mjs skills install --agent claude --dry-run",
|
|
16
16
|
"validate:skill": "python3 /Users/dougie/.codex/skills/.system/skill-creator/scripts/quick_validate.py servicemap"
|
|
17
17
|
},
|
|
18
18
|
"keywords": [
|
package/servicemap/SKILL.md
CHANGED
|
@@ -14,13 +14,15 @@ If no website can be found, create a desktop app fallback at `servicemap-desktop
|
|
|
14
14
|
## Workflow
|
|
15
15
|
|
|
16
16
|
1. Inspect the project layout and current web framework.
|
|
17
|
-
2.
|
|
18
|
-
3.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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`.
|
|
19
|
+
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.
|
|
24
26
|
|
|
25
27
|
## Safety Rules
|
|
26
28
|
|
|
@@ -30,6 +32,9 @@ The scan output may include env var names such as `STRIPE_SECRET_KEY`, but not t
|
|
|
30
32
|
|
|
31
33
|
Do not add authentication, deployment, analytics, or external writes unless the user explicitly asks.
|
|
32
34
|
If the repository already has route protection conventions for internal pages, follow them.
|
|
35
|
+
When email access is requested, enforce it only through the app's existing server-side authentication.
|
|
36
|
+
The built-in generator can enforce email allowlists for Next.js App Router projects using Clerk by checking `currentUser()` against the configured emails.
|
|
37
|
+
For unsupported frameworks or static output, report that the allowlist is recorded but not enforced.
|
|
33
38
|
If no website route can be safely attached, create the desktop app fallback instead of forcing website files into an unrelated project.
|
|
34
39
|
|
|
35
40
|
## Supporting References
|
|
@@ -70,6 +70,7 @@ function pageCss() {
|
|
|
70
70
|
.service-name span, .evidence { color: #64748b; font-size: 12px; }
|
|
71
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
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; }
|
|
73
74
|
.empty { background: #fff; border: 1px dashed #cbd5e1; border-radius: 8px; padding: 24px; }
|
|
74
75
|
@media (max-width: 720px) { .servicemap-header { display: block; } .summary { grid-template-columns: 1fr; } }
|
|
75
76
|
`;
|
|
@@ -77,6 +78,11 @@ function pageCss() {
|
|
|
77
78
|
|
|
78
79
|
function htmlBody(graph) {
|
|
79
80
|
const services = graph.services ?? [];
|
|
81
|
+
const access = graph.access ?? {};
|
|
82
|
+
const hasAccessRequest = access.requested && access.allowedEmails?.length;
|
|
83
|
+
const accessNotice = hasAccessRequest && access.enforced !== true
|
|
84
|
+
? '<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
|
+
: '';
|
|
80
86
|
const serviceCards = services.length
|
|
81
87
|
? services.map((service) => {
|
|
82
88
|
const evidence = service.evidence?.[0];
|
|
@@ -111,6 +117,7 @@ function htmlBody(graph) {
|
|
|
111
117
|
<div class="metric"><strong>${graph.summary?.highConfidenceCount ?? 0}</strong><span>high confidence</span></div>
|
|
112
118
|
<div class="metric"><strong>${escapeHtml(graph.project?.framework ?? 'unknown')}</strong><span>framework</span></div>
|
|
113
119
|
</section>
|
|
120
|
+
${accessNotice}
|
|
114
121
|
<section class="service-grid">
|
|
115
122
|
${serviceCards}
|
|
116
123
|
</section>
|
|
@@ -136,6 +143,36 @@ ${htmlBody(graph)}
|
|
|
136
143
|
|
|
137
144
|
function tsxPage(graph) {
|
|
138
145
|
const html = htmlBody(graph);
|
|
146
|
+
const allowedEmails = graph.access?.allowedEmails ?? [];
|
|
147
|
+
const usesClerk = (graph.services ?? []).some((service) => service.key === 'clerk');
|
|
148
|
+
const canEnforce = allowedEmails.length > 0 && usesClerk && graph.project?.framework === 'next-app-router';
|
|
149
|
+
if (canEnforce) {
|
|
150
|
+
graph.access.enforced = true;
|
|
151
|
+
const guardedHtml = htmlBody(graph);
|
|
152
|
+
return `import { currentUser } from '@clerk/nextjs/server';
|
|
153
|
+
import { notFound } from 'next/navigation';
|
|
154
|
+
|
|
155
|
+
const css = ${JSON.stringify(pageCss())};
|
|
156
|
+
const html = ${JSON.stringify(guardedHtml)};
|
|
157
|
+
const allowedEmails = ${JSON.stringify(allowedEmails)};
|
|
158
|
+
|
|
159
|
+
export default async function InternalServiceMapPage() {
|
|
160
|
+
const user = await currentUser();
|
|
161
|
+
const userEmails = user?.emailAddresses?.map((email) => email.emailAddress.toLowerCase()) ?? [];
|
|
162
|
+
const canView = userEmails.some((email) => allowedEmails.includes(email));
|
|
163
|
+
|
|
164
|
+
if (!canView) notFound();
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<>
|
|
168
|
+
<style dangerouslySetInnerHTML={{ __html: css }} />
|
|
169
|
+
<div dangerouslySetInnerHTML={{ __html: html }} />
|
|
170
|
+
</>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
`;
|
|
174
|
+
}
|
|
175
|
+
|
|
139
176
|
return `const css = ${JSON.stringify(pageCss())};
|
|
140
177
|
const html = ${JSON.stringify(html)};
|
|
141
178
|
|
|
@@ -65,16 +65,26 @@ function service(name, key, category, packages, envPrefixes, domains) {
|
|
|
65
65
|
|
|
66
66
|
function parseArgs() {
|
|
67
67
|
const args = process.argv.slice(2);
|
|
68
|
-
const options = { root: process.cwd(), output: defaultOutput };
|
|
68
|
+
const options = { root: process.cwd(), output: defaultOutput, allowedEmails: [] };
|
|
69
69
|
for (let index = 0; index < args.length; index += 1) {
|
|
70
70
|
if (args[index] === '--root') options.root = args[index + 1] ?? options.root;
|
|
71
71
|
if (args[index] === '--output') options.output = args[index + 1] ?? options.output;
|
|
72
|
+
if (args[index] === '--allow-email') options.allowedEmails.push(args[index + 1] ?? '');
|
|
73
|
+
if (args[index] === '--allow-emails') options.allowedEmails.push(...(args[index + 1] ?? '').split(','));
|
|
72
74
|
}
|
|
73
75
|
options.root = resolve(options.root);
|
|
74
76
|
options.output = resolve(options.root, options.output);
|
|
77
|
+
options.allowedEmails = normalizeEmails(options.allowedEmails);
|
|
75
78
|
return options;
|
|
76
79
|
}
|
|
77
80
|
|
|
81
|
+
function normalizeEmails(values) {
|
|
82
|
+
return [...new Set(values
|
|
83
|
+
.map((value) => value.trim().toLowerCase())
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
.filter((value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)))];
|
|
86
|
+
}
|
|
87
|
+
|
|
78
88
|
async function readJson(path) {
|
|
79
89
|
try {
|
|
80
90
|
return JSON.parse(await readFile(path, 'utf8'));
|
|
@@ -268,6 +278,10 @@ async function main() {
|
|
|
268
278
|
root: options.root,
|
|
269
279
|
framework: detectFramework(options.root, packageJson, files),
|
|
270
280
|
},
|
|
281
|
+
access: {
|
|
282
|
+
allowedEmails: options.allowedEmails,
|
|
283
|
+
requested: options.allowedEmails.length > 0,
|
|
284
|
+
},
|
|
271
285
|
services,
|
|
272
286
|
edges: buildEdges(projectName, services),
|
|
273
287
|
summary: {
|