servicemap 0.1.12 → 0.1.14
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/bin/servicemap.mjs +173 -26
- package/package.json +1 -1
package/bin/servicemap.mjs
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
|
-
import { cp, mkdir, readFile, rm } from 'node:fs/promises';
|
|
4
|
+
import { cp, mkdir, readFile, rm, symlink } from 'node:fs/promises';
|
|
5
5
|
import { dirname, join, resolve } from 'node:path';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import readline from 'node:readline';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
|
|
10
10
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
|
+
const repositoryUrl = 'https://github.com/douglasbrown11/servicemap.git';
|
|
12
|
+
const stableSourceRoot = () => resolve(homedir(), '.servicemap/source');
|
|
11
13
|
const scanScript = resolve(packageRoot, 'servicemap/scripts/scan-project.mjs');
|
|
12
14
|
const pageScript = resolve(packageRoot, 'servicemap/scripts/generate-page.mjs');
|
|
13
15
|
const desktopScript = resolve(packageRoot, 'servicemap/scripts/generate-desktop-app.mjs');
|
|
@@ -162,6 +164,7 @@ function parseArgs() {
|
|
|
162
164
|
agent: 'codex',
|
|
163
165
|
targets: null,
|
|
164
166
|
skillsDir: null,
|
|
167
|
+
installMethod: null,
|
|
165
168
|
allowedEmails: [],
|
|
166
169
|
invokedWithoutArgs: args.length === 0,
|
|
167
170
|
explicitCommand: false,
|
|
@@ -190,6 +193,7 @@ function parseArgs() {
|
|
|
190
193
|
}
|
|
191
194
|
if (arg === '--agent') options.agent = args[++index] ?? options.agent;
|
|
192
195
|
if (arg === '--targets') options.targets = (args[++index] ?? '').split(',').map((target) => target.trim()).filter(Boolean);
|
|
196
|
+
if (arg === '--install-method') options.installMethod = args[++index] ?? null;
|
|
193
197
|
if (arg === '--allow-email') {
|
|
194
198
|
options.allowedEmails.push(args[++index] ?? '');
|
|
195
199
|
options.generationRequested = true;
|
|
@@ -217,7 +221,7 @@ Usage:
|
|
|
217
221
|
servicemap
|
|
218
222
|
servicemap generate [--root <project>] [--mode auto|page|desktop] [--allow-emails <list>] [--dry-run]
|
|
219
223
|
servicemap --root <project> [--mode auto|page|desktop] [--allow-emails <list>] [--dry-run]
|
|
220
|
-
servicemap install-skill [--survey] [--agent codex|claude|grok|all] [--targets <ids>]
|
|
224
|
+
servicemap install-skill [--survey] [--agent codex|claude|grok|all] [--targets <ids>] [--install-method copy|symlink]
|
|
221
225
|
servicemap skills install [--survey] [--agent codex|claude|grok|all]
|
|
222
226
|
|
|
223
227
|
Examples:
|
|
@@ -291,20 +295,42 @@ function skillRootsForTargets(options, targets) {
|
|
|
291
295
|
|
|
292
296
|
const visibleUniversalLimit = 12;
|
|
293
297
|
const additionalAgentWindowSize = 8;
|
|
298
|
+
const dim = (value) => `\x1b[2m${value}\x1b[22m`;
|
|
299
|
+
const green = (value) => `\x1b[32m${value}\x1b[39m`;
|
|
300
|
+
const cyan = (value) => `\x1b[36m${value}\x1b[39m`;
|
|
301
|
+
|
|
302
|
+
function timelineStep(text) {
|
|
303
|
+
console.log(`${green('◇')} ${text}`);
|
|
304
|
+
console.log(dim('│'));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function timelineActive(text) {
|
|
308
|
+
console.log(`${green('◆')} ${text}`);
|
|
309
|
+
console.log(dim('│'));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function timelineLine(text = '') {
|
|
313
|
+
console.log(`${dim('│')} ${text}`);
|
|
314
|
+
}
|
|
294
315
|
|
|
295
316
|
function renderSurvey(selected, cursor) {
|
|
296
317
|
console.clear();
|
|
297
|
-
|
|
298
|
-
|
|
318
|
+
timelineStep(cyan('servicemap'));
|
|
319
|
+
timelineStep(`Source: ${repositoryUrl}`);
|
|
320
|
+
timelineStep(`Stable source: ${stableSourceRoot()}`);
|
|
321
|
+
timelineActive('Which agents do you want to install Servicemap to?');
|
|
322
|
+
timelineLine('Universal (.agents/skills) — always included');
|
|
299
323
|
for (const agent of universalAgents.slice(0, visibleUniversalLimit)) {
|
|
300
|
-
|
|
324
|
+
timelineLine(`• ${agent}`);
|
|
301
325
|
}
|
|
302
326
|
const hiddenUniversalCount = universalAgents.length - visibleUniversalLimit;
|
|
303
327
|
if (hiddenUniversalCount > 0) {
|
|
304
|
-
|
|
328
|
+
timelineLine(`...and ${hiddenUniversalCount} more`);
|
|
305
329
|
}
|
|
306
|
-
|
|
307
|
-
|
|
330
|
+
timelineLine('');
|
|
331
|
+
timelineLine('Additional agents');
|
|
332
|
+
timelineLine('Use ↑/↓, Space to select, Enter to install.');
|
|
333
|
+
timelineLine('');
|
|
308
334
|
|
|
309
335
|
const halfWindow = Math.floor(additionalAgentWindowSize / 2);
|
|
310
336
|
let start = Math.max(0, cursor - halfWindow);
|
|
@@ -313,19 +339,17 @@ function renderSurvey(selected, cursor) {
|
|
|
313
339
|
const beforeCount = start;
|
|
314
340
|
const afterCount = targetCatalog.length - end;
|
|
315
341
|
|
|
316
|
-
if (beforeCount > 0) {
|
|
317
|
-
console.log(` ↑ ${beforeCount} more`);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
342
|
targetCatalog.slice(start, end).forEach((target, offset) => {
|
|
321
343
|
const index = start + offset;
|
|
322
344
|
const pointer = index === cursor ? '>' : ' ';
|
|
323
345
|
const checked = selected.has(target.id) ? 'x' : ' ';
|
|
324
|
-
|
|
346
|
+
timelineLine(`${pointer} [${checked}] ${target.label} ${dim(`(${target.root()})`)}`);
|
|
325
347
|
});
|
|
326
348
|
|
|
327
|
-
if (afterCount > 0) {
|
|
328
|
-
|
|
349
|
+
if (beforeCount > 0 || afterCount > 0) {
|
|
350
|
+
const before = beforeCount > 0 ? `↑ ${beforeCount} more` : '';
|
|
351
|
+
const after = afterCount > 0 ? `↓ ${afterCount} more` : '';
|
|
352
|
+
timelineLine(dim([before, after].filter(Boolean).join(' ')));
|
|
329
353
|
}
|
|
330
354
|
|
|
331
355
|
if (selected.size > 0) {
|
|
@@ -334,7 +358,8 @@ function renderSurvey(selected, cursor) {
|
|
|
334
358
|
.map((target) => target.label);
|
|
335
359
|
const preview = selectedLabels.slice(0, 3).join(', ');
|
|
336
360
|
const remainder = selectedLabels.length > 3 ? ` +${selectedLabels.length - 3} more` : '';
|
|
337
|
-
|
|
361
|
+
timelineLine('');
|
|
362
|
+
timelineLine(`Selected: ${preview}${remainder}`);
|
|
338
363
|
}
|
|
339
364
|
}
|
|
340
365
|
|
|
@@ -379,12 +404,134 @@ function runTargetSurvey() {
|
|
|
379
404
|
});
|
|
380
405
|
}
|
|
381
406
|
|
|
407
|
+
function renderInstallMethodSurvey(cursor) {
|
|
408
|
+
console.clear();
|
|
409
|
+
const methods = [
|
|
410
|
+
{ id: 'symlink', label: 'Symlink', note: 'Recommended, one stable source for easy updates' },
|
|
411
|
+
{ id: 'copy', label: 'Copy', note: 'Standalone copies in each selected agent folder' },
|
|
412
|
+
];
|
|
413
|
+
|
|
414
|
+
timelineStep(cyan('servicemap'));
|
|
415
|
+
timelineStep(`Source: ${repositoryUrl}`);
|
|
416
|
+
timelineStep(`Stable source: ${stableSourceRoot()}`);
|
|
417
|
+
timelineStep('Agents selected');
|
|
418
|
+
timelineActive('Installation method');
|
|
419
|
+
methods.forEach((method, index) => {
|
|
420
|
+
const pointer = index === cursor ? '>' : ' ';
|
|
421
|
+
const selected = index === cursor ? '●' : '○';
|
|
422
|
+
timelineLine(`${pointer} ${selected} ${method.label} ${dim(`(${method.note})`)}`);
|
|
423
|
+
});
|
|
424
|
+
timelineLine(dim('↑/↓ to navigate • Enter to confirm'));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function runInstallMethodSurvey() {
|
|
428
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
429
|
+
return Promise.resolve('copy');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const methods = ['symlink', 'copy'];
|
|
433
|
+
let cursor = 0;
|
|
434
|
+
|
|
435
|
+
return new Promise((resolvePromise) => {
|
|
436
|
+
readline.emitKeypressEvents(process.stdin);
|
|
437
|
+
process.stdin.setRawMode(true);
|
|
438
|
+
renderInstallMethodSurvey(cursor);
|
|
439
|
+
|
|
440
|
+
const onKeypress = (_str, key) => {
|
|
441
|
+
if (key.name === 'up') cursor = Math.max(0, cursor - 1);
|
|
442
|
+
if (key.name === 'down') cursor = Math.min(methods.length - 1, cursor + 1);
|
|
443
|
+
if (key.name === 'return') {
|
|
444
|
+
process.stdin.setRawMode(false);
|
|
445
|
+
process.stdin.off('keypress', onKeypress);
|
|
446
|
+
console.log('');
|
|
447
|
+
resolvePromise(methods[cursor]);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (key.ctrl && key.name === 'c') {
|
|
451
|
+
process.stdin.setRawMode(false);
|
|
452
|
+
process.stdin.off('keypress', onKeypress);
|
|
453
|
+
process.exit(130);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
renderInstallMethodSurvey(cursor);
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
process.stdin.on('keypress', onKeypress);
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function runSilent(command, args, options = {}) {
|
|
464
|
+
return new Promise((resolvePromise, reject) => {
|
|
465
|
+
const child = spawn(command, args, {
|
|
466
|
+
stdio: 'ignore',
|
|
467
|
+
...options,
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
child.on('error', reject);
|
|
471
|
+
child.on('exit', (code) => {
|
|
472
|
+
if (code === 0) resolvePromise();
|
|
473
|
+
else reject(new Error(`${command} ${args.join(' ')} exited with code ${code}`));
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function prepareStableSource(options) {
|
|
479
|
+
const sourceRoot = stableSourceRoot();
|
|
480
|
+
if (options.dryRun) {
|
|
481
|
+
timelineStep(`Would prepare stable source at ${sourceRoot}`);
|
|
482
|
+
return resolve(sourceRoot, 'servicemap');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
timelineStep(`Source: ${repositoryUrl}`);
|
|
486
|
+
await mkdir(dirname(sourceRoot), { recursive: true });
|
|
487
|
+
|
|
488
|
+
try {
|
|
489
|
+
if (existsSync(join(sourceRoot, '.git'))) {
|
|
490
|
+
await runSilent('git', ['pull', '--ff-only'], { cwd: sourceRoot });
|
|
491
|
+
timelineStep('Repository updated');
|
|
492
|
+
} else {
|
|
493
|
+
await rm(sourceRoot, { recursive: true, force: true });
|
|
494
|
+
await runSilent('git', ['clone', '--depth', '1', repositoryUrl, sourceRoot]);
|
|
495
|
+
timelineStep('Repository cloned');
|
|
496
|
+
}
|
|
497
|
+
} catch (_error) {
|
|
498
|
+
await rm(sourceRoot, { recursive: true, force: true });
|
|
499
|
+
await cp(packageRoot, sourceRoot, { recursive: true });
|
|
500
|
+
timelineStep('Repository unavailable; copied packaged source');
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return resolve(sourceRoot, 'servicemap');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function installIntoRoot(root, source, method, options) {
|
|
507
|
+
const destination = join(root, 'servicemap');
|
|
508
|
+
if (options.dryRun) {
|
|
509
|
+
const verb = method === 'symlink' ? 'link' : 'copy';
|
|
510
|
+
console.log(`Would ${verb} Servicemap skill to ${destination}`);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
await mkdir(root, { recursive: true });
|
|
515
|
+
await rm(destination, { recursive: true, force: true });
|
|
516
|
+
|
|
517
|
+
if (method === 'symlink') {
|
|
518
|
+
await symlink(source, destination, 'dir');
|
|
519
|
+
console.log(`Linked Servicemap skill to ${destination}`);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
await cp(source, destination, { recursive: true });
|
|
524
|
+
console.log(`Installed Servicemap skill to ${destination}`);
|
|
525
|
+
}
|
|
526
|
+
|
|
382
527
|
async function installSkill(options) {
|
|
383
|
-
|
|
528
|
+
let source = resolve(packageRoot, 'servicemap');
|
|
384
529
|
const selectedTargets = options.survey && !options.yes
|
|
385
530
|
? [{ id: 'universal', label: 'Universal', root: universalRoot }, ...await runTargetSurvey()]
|
|
386
531
|
: targetsForOptions(options);
|
|
387
532
|
const roots = skillRootsForTargets(options, selectedTargets);
|
|
533
|
+
const method = options.installMethod
|
|
534
|
+
?? (options.survey && !options.yes ? await runInstallMethodSurvey() : 'copy');
|
|
388
535
|
|
|
389
536
|
if (!existsSync(source)) {
|
|
390
537
|
throw new Error(`Bundled skill folder not found: ${source}`);
|
|
@@ -395,19 +542,19 @@ async function installSkill(options) {
|
|
|
395
542
|
return;
|
|
396
543
|
}
|
|
397
544
|
|
|
398
|
-
if (
|
|
399
|
-
|
|
400
|
-
|
|
545
|
+
if (!['copy', 'symlink'].includes(method)) {
|
|
546
|
+
throw new Error(`Unknown install method "${method}". Use copy or symlink.`);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (method === 'symlink') {
|
|
550
|
+
source = await prepareStableSource(options);
|
|
551
|
+
if (!existsSync(source) && !options.dryRun) {
|
|
552
|
+
throw new Error(`Stable skill source not found: ${source}`);
|
|
401
553
|
}
|
|
402
|
-
return;
|
|
403
554
|
}
|
|
404
555
|
|
|
405
556
|
for (const root of roots) {
|
|
406
|
-
|
|
407
|
-
await mkdir(root, { recursive: true });
|
|
408
|
-
await rm(destination, { recursive: true, force: true });
|
|
409
|
-
await cp(source, destination, { recursive: true });
|
|
410
|
-
console.log(`Installed Servicemap skill to ${destination}`);
|
|
557
|
+
await installIntoRoot(root, source, method, options);
|
|
411
558
|
}
|
|
412
559
|
|
|
413
560
|
console.log('Restart your AI app if /servicemap does not appear immediately.');
|