servicemap 0.1.13 → 0.1.15
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 +192 -21
- 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:
|
|
@@ -292,20 +296,41 @@ function skillRootsForTargets(options, targets) {
|
|
|
292
296
|
const visibleUniversalLimit = 12;
|
|
293
297
|
const additionalAgentWindowSize = 8;
|
|
294
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
|
+
}
|
|
295
315
|
|
|
296
316
|
function renderSurvey(selected, cursor) {
|
|
297
317
|
console.clear();
|
|
298
|
-
|
|
299
|
-
|
|
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');
|
|
300
323
|
for (const agent of universalAgents.slice(0, visibleUniversalLimit)) {
|
|
301
|
-
|
|
324
|
+
timelineLine(`• ${agent}`);
|
|
302
325
|
}
|
|
303
326
|
const hiddenUniversalCount = universalAgents.length - visibleUniversalLimit;
|
|
304
327
|
if (hiddenUniversalCount > 0) {
|
|
305
|
-
|
|
328
|
+
timelineLine(`...and ${hiddenUniversalCount} more`);
|
|
306
329
|
}
|
|
307
|
-
|
|
308
|
-
|
|
330
|
+
timelineLine('');
|
|
331
|
+
timelineLine('Additional agents');
|
|
332
|
+
timelineLine('Use ↑/↓, Space to select, Enter to install.');
|
|
333
|
+
timelineLine('');
|
|
309
334
|
|
|
310
335
|
const halfWindow = Math.floor(additionalAgentWindowSize / 2);
|
|
311
336
|
let start = Math.max(0, cursor - halfWindow);
|
|
@@ -318,13 +343,13 @@ function renderSurvey(selected, cursor) {
|
|
|
318
343
|
const index = start + offset;
|
|
319
344
|
const pointer = index === cursor ? '>' : ' ';
|
|
320
345
|
const checked = selected.has(target.id) ? 'x' : ' ';
|
|
321
|
-
|
|
346
|
+
timelineLine(`${pointer} [${checked}] ${target.label} ${dim(`(${target.root()})`)}`);
|
|
322
347
|
});
|
|
323
348
|
|
|
324
349
|
if (beforeCount > 0 || afterCount > 0) {
|
|
325
350
|
const before = beforeCount > 0 ? `↑ ${beforeCount} more` : '';
|
|
326
351
|
const after = afterCount > 0 ? `↓ ${afterCount} more` : '';
|
|
327
|
-
|
|
352
|
+
timelineLine(dim([before, after].filter(Boolean).join(' ')));
|
|
328
353
|
}
|
|
329
354
|
|
|
330
355
|
if (selected.size > 0) {
|
|
@@ -333,7 +358,8 @@ function renderSurvey(selected, cursor) {
|
|
|
333
358
|
.map((target) => target.label);
|
|
334
359
|
const preview = selectedLabels.slice(0, 3).join(', ');
|
|
335
360
|
const remainder = selectedLabels.length > 3 ? ` +${selectedLabels.length - 3} more` : '';
|
|
336
|
-
|
|
361
|
+
timelineLine('');
|
|
362
|
+
timelineLine(`Selected: ${preview}${remainder}`);
|
|
337
363
|
}
|
|
338
364
|
}
|
|
339
365
|
|
|
@@ -378,12 +404,157 @@ function runTargetSurvey() {
|
|
|
378
404
|
});
|
|
379
405
|
}
|
|
380
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 withSpinner(message, action) {
|
|
479
|
+
if (!process.stdout.isTTY) {
|
|
480
|
+
return action();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const frames = ['◒', '◐', '◓', '◑'];
|
|
484
|
+
let frame = 0;
|
|
485
|
+
let dots = '';
|
|
486
|
+
process.stdout.write('\x1b[?25l');
|
|
487
|
+
const interval = setInterval(() => {
|
|
488
|
+
process.stdout.write(`\r\x1b[J${frames[frame]} ${message}${dots}`);
|
|
489
|
+
frame = (frame + 1) % frames.length;
|
|
490
|
+
if (frame === 0) dots = dots.length >= 5 ? '' : `${dots}.`;
|
|
491
|
+
}, 120);
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
return await action();
|
|
495
|
+
} finally {
|
|
496
|
+
clearInterval(interval);
|
|
497
|
+
process.stdout.write('\r\x1b[J\x1b[?25h');
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async function prepareStableSource(options) {
|
|
502
|
+
const sourceRoot = stableSourceRoot();
|
|
503
|
+
if (options.dryRun) {
|
|
504
|
+
timelineStep(`Would prepare stable source at ${sourceRoot}`);
|
|
505
|
+
return resolve(sourceRoot, 'servicemap');
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
timelineStep(`Source: ${repositoryUrl}`);
|
|
509
|
+
await mkdir(dirname(sourceRoot), { recursive: true });
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
if (existsSync(join(sourceRoot, '.git'))) {
|
|
513
|
+
await withSpinner('Updating repository…', () => runSilent('git', ['pull', '--ff-only'], { cwd: sourceRoot }));
|
|
514
|
+
timelineStep('Repository updated');
|
|
515
|
+
} else {
|
|
516
|
+
await rm(sourceRoot, { recursive: true, force: true });
|
|
517
|
+
await withSpinner('Cloning repository…', () => runSilent('git', ['clone', '--depth', '1', repositoryUrl, sourceRoot]));
|
|
518
|
+
timelineStep('Repository cloned');
|
|
519
|
+
}
|
|
520
|
+
} catch (_error) {
|
|
521
|
+
await rm(sourceRoot, { recursive: true, force: true });
|
|
522
|
+
await cp(packageRoot, sourceRoot, { recursive: true });
|
|
523
|
+
timelineStep('Repository unavailable; copied packaged source');
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return resolve(sourceRoot, 'servicemap');
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function installIntoRoot(root, source, method, options) {
|
|
530
|
+
const destination = join(root, 'servicemap');
|
|
531
|
+
if (options.dryRun) {
|
|
532
|
+
const verb = method === 'symlink' ? 'link' : 'copy';
|
|
533
|
+
console.log(`Would ${verb} Servicemap skill to ${destination}`);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
await mkdir(root, { recursive: true });
|
|
538
|
+
await rm(destination, { recursive: true, force: true });
|
|
539
|
+
|
|
540
|
+
if (method === 'symlink') {
|
|
541
|
+
await symlink(source, destination, 'dir');
|
|
542
|
+
console.log(`Linked Servicemap skill to ${destination}`);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
await cp(source, destination, { recursive: true });
|
|
547
|
+
console.log(`Installed Servicemap skill to ${destination}`);
|
|
548
|
+
}
|
|
549
|
+
|
|
381
550
|
async function installSkill(options) {
|
|
382
|
-
|
|
551
|
+
let source = resolve(packageRoot, 'servicemap');
|
|
383
552
|
const selectedTargets = options.survey && !options.yes
|
|
384
553
|
? [{ id: 'universal', label: 'Universal', root: universalRoot }, ...await runTargetSurvey()]
|
|
385
554
|
: targetsForOptions(options);
|
|
386
555
|
const roots = skillRootsForTargets(options, selectedTargets);
|
|
556
|
+
const method = options.installMethod
|
|
557
|
+
?? (options.survey && !options.yes ? await runInstallMethodSurvey() : 'copy');
|
|
387
558
|
|
|
388
559
|
if (!existsSync(source)) {
|
|
389
560
|
throw new Error(`Bundled skill folder not found: ${source}`);
|
|
@@ -394,19 +565,19 @@ async function installSkill(options) {
|
|
|
394
565
|
return;
|
|
395
566
|
}
|
|
396
567
|
|
|
397
|
-
if (
|
|
398
|
-
|
|
399
|
-
|
|
568
|
+
if (!['copy', 'symlink'].includes(method)) {
|
|
569
|
+
throw new Error(`Unknown install method "${method}". Use copy or symlink.`);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (method === 'symlink') {
|
|
573
|
+
source = await prepareStableSource(options);
|
|
574
|
+
if (!existsSync(source) && !options.dryRun) {
|
|
575
|
+
throw new Error(`Stable skill source not found: ${source}`);
|
|
400
576
|
}
|
|
401
|
-
return;
|
|
402
577
|
}
|
|
403
578
|
|
|
404
579
|
for (const root of roots) {
|
|
405
|
-
|
|
406
|
-
await mkdir(root, { recursive: true });
|
|
407
|
-
await rm(destination, { recursive: true, force: true });
|
|
408
|
-
await cp(source, destination, { recursive: true });
|
|
409
|
-
console.log(`Installed Servicemap skill to ${destination}`);
|
|
580
|
+
await installIntoRoot(root, source, method, options);
|
|
410
581
|
}
|
|
411
582
|
|
|
412
583
|
console.log('Restart your AI app if /servicemap does not appear immediately.');
|