servicemap 0.1.0 → 0.1.4

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
@@ -6,12 +6,43 @@ The skill scans a software project for external services, resolves service logos
6
6
 
7
7
  ## Terminal Use
8
8
 
9
- The npm package name `servicemap` is available, so users can run the clean command:
9
+ The npm package name `servicemap` is available, so users can run the clean command and choose where to install it:
10
10
 
11
11
  ```bash
12
12
  npx servicemap
13
13
  ```
14
14
 
15
+ That opens the interactive install survey.
16
+ It lets users choose compatible AI coding agents and model environments.
17
+
18
+ To install the bundled AI skill so `/servicemap` appears in compatible coding agents:
19
+
20
+ ```bash
21
+ npx servicemap install-skill
22
+ ```
23
+
24
+ To use an interactive Caveman-style terminal survey:
25
+
26
+ ```bash
27
+ npx servicemap install-skill --survey
28
+ ```
29
+
30
+ That installs to the Codex-style user skill folder, matching the way Caveman installs a local skill folder.
31
+ To install for several agents on the same computer:
32
+
33
+ ```bash
34
+ npx servicemap install-skill --agent all
35
+ ```
36
+
37
+ The Caveman-style command shape also works:
38
+
39
+ ```bash
40
+ npx servicemap skills install --agent claude
41
+ ```
42
+
43
+ Agents with known local skill folders get the real `servicemap` skill installed.
44
+ Other selected AI tools get a portable guide pack under `~/.servicemap/model-guides`.
45
+
15
46
  After a global install, the command is:
16
47
 
17
48
  ```bash
@@ -27,10 +58,20 @@ npm install -g servicemap
27
58
  CLI options:
28
59
 
29
60
  ```bash
61
+ servicemap
62
+ servicemap generate --root <project-path>
30
63
  servicemap --root <project-path>
31
64
  servicemap --mode page
32
65
  servicemap --mode desktop
33
66
  servicemap --dry-run
67
+ servicemap install-skill
68
+ servicemap install-skill --survey
69
+ servicemap install-skill --agent codex
70
+ servicemap install-skill --agent claude
71
+ servicemap install-skill --agent grok
72
+ servicemap install-skill --agent all
73
+ servicemap install-skill --targets codex,chatgpt,cursor
74
+ servicemap skills install --agent codex
34
75
  ```
35
76
 
36
77
  ## Contents
@@ -45,7 +86,11 @@ servicemap --dry-run
45
86
  ## Validation
46
87
 
47
88
  ```bash
48
- node bin/servicemap.mjs --root demo-project --dry-run
89
+ node bin/servicemap.mjs generate --root demo-project --dry-run
49
90
  node bin/servicemap.mjs --root no-website-demo --dry-run
91
+ node bin/servicemap.mjs --dry-run
92
+ node bin/servicemap.mjs install-skill --agent all --dry-run
93
+ node bin/servicemap.mjs install-skill --targets codex,chatgpt,cursor --dry-run
94
+ node bin/servicemap.mjs skills install --agent claude --dry-run
50
95
  python3 /Users/dougie/.codex/skills/.system/skill-creator/scripts/quick_validate.py servicemap
51
96
  ```
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from 'node:child_process';
3
- import { readFile } from 'node:fs/promises';
4
- import { dirname, resolve } from 'node:path';
3
+ import { existsSync } from 'node:fs';
4
+ import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { homedir } from 'node:os';
7
+ import readline from 'node:readline';
5
8
  import { fileURLToPath } from 'node:url';
6
9
 
7
10
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
@@ -9,18 +12,100 @@ const scanScript = resolve(packageRoot, 'servicemap/scripts/scan-project.mjs');
9
12
  const pageScript = resolve(packageRoot, 'servicemap/scripts/generate-page.mjs');
10
13
  const desktopScript = resolve(packageRoot, 'servicemap/scripts/generate-desktop-app.mjs');
11
14
 
15
+ const targetCatalog = [
16
+ { id: 'codex', label: 'Codex / ChatGPT desktop', kind: 'skill', root: () => resolve(homedir(), '.agents/skills'), recommended: true },
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: 'chatgpt', label: 'ChatGPT', kind: 'guide' },
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' },
31
+ { id: 'cursor', label: 'Cursor', kind: 'guide' },
32
+ { id: 'windsurf', label: 'Windsurf', kind: 'guide' },
33
+ { id: 'copilot', label: 'GitHub Copilot', kind: 'guide' },
34
+ { id: 'vscode-chat', label: 'VS Code Chat', kind: 'guide' },
35
+ { id: 'continue', label: 'Continue.dev', kind: 'guide' },
36
+ { id: 'aider', label: 'Aider', kind: 'guide' },
37
+ { id: 'cline', label: 'Cline', kind: 'guide' },
38
+ { id: 'roo-code', label: 'Roo Code', kind: 'guide' },
39
+ { id: 'zed-ai', label: 'Zed AI', kind: 'guide' },
40
+ { id: 'replit-agent', label: 'Replit Agent', kind: 'guide' },
41
+ { id: 'lovable', label: 'Lovable', kind: 'guide' },
42
+ { id: 'bolt', label: 'Bolt', kind: 'guide' },
43
+ { id: 'v0', label: 'v0', kind: 'guide' },
44
+ { id: 'perplexity', label: 'Perplexity', kind: 'guide' },
45
+ { id: 'mistral', label: 'Mistral Le Chat', kind: 'guide' },
46
+ { id: 'deepseek', label: 'DeepSeek', kind: 'guide' },
47
+ { id: 'qwen', label: 'Qwen', kind: 'guide' },
48
+ { id: 'llama', label: 'Llama', kind: 'guide' },
49
+ { id: 'ollama', label: 'Ollama', kind: 'guide' },
50
+ { id: 'lm-studio', label: 'LM Studio', kind: 'guide' },
51
+ { 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
+ { id: 'bedrock', label: 'Amazon Bedrock', kind: 'guide' },
56
+ { id: 'azure-openai', label: 'Azure OpenAI', kind: 'guide' },
57
+ { id: 'vertex-ai', label: 'Vertex AI', kind: 'guide' },
58
+ { id: 'poe', label: 'Poe', kind: 'guide' },
59
+ { id: 'you-com', label: 'You.com', kind: 'guide' },
60
+ { id: 'phind', label: 'Phind', kind: 'guide' },
61
+ { id: 'sourcegraph-cody', label: 'Sourcegraph Cody', kind: 'guide' },
62
+ { id: 'tabnine', label: 'Tabnine', kind: 'guide' },
63
+ { id: 'codeium', label: 'Codeium', kind: 'guide' },
64
+ { id: 'jetbrains-ai', label: 'JetBrains AI', kind: 'guide' },
65
+ { id: 'custom', label: 'Other custom model or agent', kind: 'guide' },
66
+ ];
67
+
12
68
  function parseArgs() {
13
69
  const args = process.argv.slice(2);
14
70
  const options = {
71
+ command: 'generate',
15
72
  root: process.cwd(),
16
73
  mode: 'auto',
17
74
  dryRun: false,
75
+ agent: 'codex',
76
+ targets: null,
77
+ skillsDir: null,
78
+ invokedWithoutArgs: args.length === 0,
79
+ explicitCommand: false,
80
+ generationRequested: false,
18
81
  };
19
82
 
83
+ if (args[0] === 'skills' && args[1] === 'install') {
84
+ options.command = 'install-skill';
85
+ args.splice(0, 2);
86
+ }
87
+
88
+ if (args[0] && !args[0].startsWith('-')) {
89
+ options.command = args.shift();
90
+ options.explicitCommand = true;
91
+ }
92
+
20
93
  for (let index = 0; index < args.length; index += 1) {
21
94
  const arg = args[index];
22
- if (arg === '--root') options.root = args[++index] ?? options.root;
23
- if (arg === '--mode') options.mode = args[++index] ?? options.mode;
95
+ if (arg === '--root') {
96
+ options.root = args[++index] ?? options.root;
97
+ options.generationRequested = true;
98
+ }
99
+ if (arg === '--mode') {
100
+ options.mode = args[++index] ?? options.mode;
101
+ options.generationRequested = true;
102
+ }
103
+ if (arg === '--agent') options.agent = args[++index] ?? options.agent;
104
+ if (arg === '--targets') options.targets = (args[++index] ?? '').split(',').map((target) => target.trim()).filter(Boolean);
105
+ if (arg === '--skills-dir') options.skillsDir = resolve(args[++index] ?? '.');
106
+ if (arg === '--user') options.user = true;
107
+ if (arg === '--survey') options.survey = true;
108
+ if (arg === '--yes' || arg === '-y') options.yes = true;
24
109
  if (arg === '--dry-run') options.dryRun = true;
25
110
  if (arg === '--help' || arg === '-h') options.help = true;
26
111
  }
@@ -33,11 +118,21 @@ function help() {
33
118
  console.log(`Servicemap
34
119
 
35
120
  Usage:
36
- servicemap [--root <project>] [--mode auto|page|desktop] [--dry-run]
121
+ servicemap
122
+ servicemap generate [--root <project>] [--mode auto|page|desktop] [--dry-run]
123
+ servicemap --root <project> [--mode auto|page|desktop] [--dry-run]
124
+ servicemap install-skill [--survey] [--agent codex|claude|grok|all] [--targets <ids>]
125
+ servicemap skills install [--survey] [--agent codex|claude|grok|all]
37
126
 
38
127
  Examples:
39
128
  npx servicemap
129
+ npx servicemap generate --root ../my-app
40
130
  npx servicemap -- --root ../my-app
131
+ npx servicemap install-skill
132
+ npx servicemap install-skill --survey
133
+ npx servicemap install-skill --agent all
134
+ npx servicemap install-skill --targets codex,claude,cursor
135
+ npx servicemap skills install --agent claude
41
136
  servicemap --mode desktop
42
137
  `);
43
138
  }
@@ -62,6 +157,160 @@ async function readScan(root) {
62
157
  return JSON.parse(await readFile(scanPath, 'utf8'));
63
158
  }
64
159
 
160
+ function targetsByIds(ids) {
161
+ const byId = new Map(targetCatalog.map((target) => [target.id, target]));
162
+ return ids.map((id) => {
163
+ const target = byId.get(id);
164
+ if (!target) {
165
+ throw new Error(`Unknown target "${id}". Use --survey to choose from the list.`);
166
+ }
167
+ return target;
168
+ });
169
+ }
170
+
171
+ function targetsForOptions(options) {
172
+ if (options.targets?.length) return targetsByIds(options.targets);
173
+ if (options.agent === 'all') return targetCatalog.filter((target) => target.kind === 'skill');
174
+ if (options.agent === 'agents') return targetsByIds(['codex']);
175
+ return targetsByIds([options.agent]);
176
+ }
177
+
178
+ function skillRootsForTargets(options, targets) {
179
+ const skillTargets = targets.filter((target) => target.kind === 'skill');
180
+ if (options.skillsDir && skillTargets.length) return [options.skillsDir];
181
+ return [...new Set(skillTargets.map((target) => target.root()))];
182
+ }
183
+
184
+ function guideTargets(targets) {
185
+ return targets.filter((target) => target.kind === 'guide');
186
+ }
187
+
188
+ function renderSurvey(selected, cursor) {
189
+ console.clear();
190
+ console.log('Where should Servicemap be available?');
191
+ console.log('Use ↑/↓, Space to select, Enter to install.\n');
192
+
193
+ targetCatalog.forEach((target, index) => {
194
+ const pointer = index === cursor ? '>' : ' ';
195
+ const checked = selected.has(target.id) ? 'x' : ' ';
196
+ const detail = target.kind === 'skill' ? 'skill folder' : 'portable guide';
197
+ console.log(`${pointer} [${checked}] ${target.label} (${detail})`);
198
+ });
199
+ }
200
+
201
+ function runTargetSurvey() {
202
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
203
+ return Promise.resolve(targetCatalog.filter((target) => target.recommended));
204
+ }
205
+
206
+ const selected = new Set(targetCatalog.filter((target) => target.recommended).map((target) => target.id));
207
+ let cursor = 0;
208
+
209
+ return new Promise((resolvePromise) => {
210
+ readline.emitKeypressEvents(process.stdin);
211
+ process.stdin.setRawMode(true);
212
+ renderSurvey(selected, cursor);
213
+
214
+ const onKeypress = (_str, key) => {
215
+ if (key.name === 'up') cursor = (cursor - 1 + targetCatalog.length) % targetCatalog.length;
216
+ if (key.name === 'down') cursor = (cursor + 1) % targetCatalog.length;
217
+ if (key.name === 'space') {
218
+ const id = targetCatalog[cursor].id;
219
+ if (selected.has(id)) selected.delete(id);
220
+ else selected.add(id);
221
+ }
222
+ if (key.name === 'return') {
223
+ process.stdin.setRawMode(false);
224
+ process.stdin.off('keypress', onKeypress);
225
+ console.log('');
226
+ resolvePromise(targetCatalog.filter((target) => selected.has(target.id)));
227
+ return;
228
+ }
229
+ if (key.ctrl && key.name === 'c') {
230
+ process.stdin.setRawMode(false);
231
+ process.stdin.off('keypress', onKeypress);
232
+ process.exit(130);
233
+ }
234
+
235
+ renderSurvey(selected, cursor);
236
+ };
237
+
238
+ process.stdin.on('keypress', onKeypress);
239
+ });
240
+ }
241
+
242
+ function guideText(target) {
243
+ return `# Servicemap for ${target.label}
244
+
245
+ Use this instruction with ${target.label} when you want it to create a service map for a software project.
246
+
247
+ Invoke: /servicemap
248
+
249
+ Goal:
250
+ Scan the current project for third-party services, resolve service logos from the bundled Servicemap database, and create an /internalservicemap page when a website is present.
251
+ If no website can be safely found, create a desktop app fallback named servicemap-desktop.
252
+
253
+ Recommended local command:
254
+ \`\`\`bash
255
+ npx servicemap --root <project-path>
256
+ \`\`\`
257
+
258
+ If your agent supports local skills, install the real skill folder with:
259
+ \`\`\`bash
260
+ npx servicemap install-skill --agent all
261
+ \`\`\`
262
+ `;
263
+ }
264
+
265
+ async function writeGuidePacks(targets, options) {
266
+ const guideRoot = resolve(homedir(), '.servicemap/model-guides');
267
+ for (const target of guideTargets(targets)) {
268
+ const destination = join(guideRoot, target.id);
269
+ if (options.dryRun) {
270
+ console.log(`Would write Servicemap guide for ${target.label} to ${destination}`);
271
+ continue;
272
+ }
273
+ await mkdir(destination, { recursive: true });
274
+ await cp(resolve(packageRoot, 'servicemap/SKILL.md'), join(destination, 'SKILL.md'));
275
+ await mkdir(join(destination, 'servicemap'), { recursive: true });
276
+ await cp(resolve(packageRoot, 'servicemap'), join(destination, 'servicemap'), { recursive: true });
277
+ await writeFile(join(destination, 'README.md'), guideText(target));
278
+ console.log(`Wrote Servicemap guide for ${target.label} to ${destination}`);
279
+ }
280
+ }
281
+
282
+ async function installSkill(options) {
283
+ const source = resolve(packageRoot, 'servicemap');
284
+ const selectedTargets = options.survey && !options.yes
285
+ ? await runTargetSurvey()
286
+ : targetsForOptions(options);
287
+ const roots = skillRootsForTargets(options, selectedTargets);
288
+
289
+ if (!existsSync(source)) {
290
+ throw new Error(`Bundled skill folder not found: ${source}`);
291
+ }
292
+
293
+ if (options.dryRun) {
294
+ for (const root of roots) {
295
+ console.log(`Would install Servicemap skill to ${join(root, 'servicemap')}`);
296
+ }
297
+ await writeGuidePacks(selectedTargets, options);
298
+ return;
299
+ }
300
+
301
+ for (const root of roots) {
302
+ const destination = join(root, 'servicemap');
303
+ await mkdir(root, { recursive: true });
304
+ await rm(destination, { recursive: true, force: true });
305
+ await cp(source, destination, { recursive: true });
306
+ console.log(`Installed Servicemap skill to ${destination}`);
307
+ }
308
+
309
+ await writeGuidePacks(selectedTargets, options);
310
+
311
+ console.log('Restart your AI app if /servicemap does not appear immediately.');
312
+ }
313
+
65
314
  async function main() {
66
315
  const options = parseArgs();
67
316
  if (options.help) {
@@ -69,6 +318,20 @@ async function main() {
69
318
  return;
70
319
  }
71
320
 
321
+ if (options.command === 'generate' && !options.explicitCommand && !options.generationRequested) {
322
+ options.command = 'install-skill';
323
+ options.survey = true;
324
+ }
325
+
326
+ if (options.command === 'install-skill' || options.command === 'install') {
327
+ await installSkill(options);
328
+ return;
329
+ }
330
+
331
+ if (options.command !== 'generate') {
332
+ throw new Error(`Unknown command "${options.command}". Use generate, install-skill, or skills install.`);
333
+ }
334
+
72
335
  if (!['auto', 'page', 'desktop'].includes(options.mode)) {
73
336
  throw new Error(`Unknown mode "${options.mode}". Use auto, page, or desktop.`);
74
337
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "servicemap",
3
- "version": "0.1.0",
3
+ "version": "0.1.4",
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 --root demo-project --dry-run && node bin/servicemap.mjs --root no-website-demo --dry-run",
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,chatgpt,cursor --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": [