@fprad0/skill-master-mcp 0.0.5 → 0.0.7

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/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ All notable changes to `skill_master` will be tracked here.
7
7
  - Prepare a public npm publication path with dedicated workflow and operator guide.
8
8
  - Upgrade release automation from Node `20` to Node `22`.
9
9
 
10
+ ## [0.0.7] - 2026-06-26
11
+
12
+ - Add a visual terminal menu for `skill-master-menu` using `prompts`, with TTY detection and direct action mode.
13
+ - Extract the menu core into a reusable module for tests and future UI changes.
14
+ - Keep the package compatible with Node `18+` instead of moving to a heavier TUI stack.
15
+
16
+ ## [0.0.6] - 2026-06-26
17
+
18
+ - Harden `skill-master-configure-private-registry` with HTTPS registry validation, scoped token handling, redacted dry-runs, timestamped backups, and POSIX `0600` permissions.
19
+ - Store `${GITHUB_PACKAGES_TOKEN}` in `.npmrc` by default instead of persisting a literal token.
20
+ - Add `--token-file`, `--token-stdin`, and explicit `--store-token` support for safer operator workflows.
21
+
10
22
  ## [0.0.5] - 2026-06-26
11
23
 
12
24
  - Add `skill-master-update` to update the global npm installation safely outside the MCP stdio process.
package/README.md CHANGED
@@ -81,6 +81,8 @@ Bootstrap direto pelo pacote, para maquinas que ja conseguem resolver o escopo `
81
81
  - PowerShell: `$env:GITHUB_PACKAGES_TOKEN='SEU_TOKEN'; npx -y -p @fprad0/skill-master-mcp skill-master-configure-private-registry --validate`
82
82
  - Bash: `GITHUB_PACKAGES_TOKEN='SEU_TOKEN' npx -y -p @fprad0/skill-master-mcp skill-master-configure-private-registry --validate`
83
83
 
84
+ Por padrao, o configurador grava o `.npmrc` usando `${GITHUB_PACKAGES_TOKEN}` em vez de persistir o token literal. Para ambientes que exigem token gravado no arquivo, use `--store-token` de forma consciente.
85
+
84
86
  Exemplo de `.npmrc`:
85
87
 
86
88
  ```ini
@@ -108,8 +110,8 @@ npx -y @fprad0/skill-master-mcp@latest
108
110
 
109
111
  Estado atual validado em `2026-06-26`:
110
112
 
111
- - pacote publico publicado: `@fprad0/skill-master-mcp@0.0.5`
112
- - dist-tag `latest` apontando para `0.0.5`
113
+ - pacote publico publicado: `@fprad0/skill-master-mcp@0.0.7`
114
+ - dist-tag `latest` apontando para `0.0.7`
113
115
  - workflow `Publish Skill Master to npmjs` concluido com sucesso
114
116
  - `npm install` e `npx` confirmados
115
117
 
@@ -128,6 +130,16 @@ O `skill-master-mcp` deve continuar reservado para clientes MCP via `stdio`. Par
128
130
  skill-master-menu
129
131
  ```
130
132
 
133
+ O menu agora possui modo visual interativo para terminal com TTY e tambem aceita execucao direta por acao:
134
+
135
+ ```bash
136
+ skill-master-menu --run status
137
+ skill-master-menu --run check
138
+ skill-master-menu --run update --yes
139
+ ```
140
+
141
+ O menu visual usa `prompts` para ficar mais legivel no terminal e manter compatibilidade com `Node 18+`.
142
+
131
143
  Para atualizar a instalacao global via npm sem iniciar o servidor MCP:
132
144
 
133
145
  ```bash
package/VERSION.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  Versão funcional planejada: `00.02`
4
4
 
5
- Versão técnica para empacotamento semântico: `0.0.5`
5
+ Versão técnica para empacotamento semântico: `0.0.7`
6
6
 
7
7
  ## Observação
8
8
 
9
- O nome `00.02` será usado na comunicação e nos documentos. Para ferramentas que exigem SemVer, como npm e alguns fluxos de release, usar `0.0.5`.
9
+ O nome `00.02` será usado na comunicação e nos documentos. Para ferramentas que exigem SemVer, como npm e alguns fluxos de release, usar `0.0.7`.
@@ -0,0 +1,227 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import process from 'node:process';
5
+
6
+ const ANSI = {
7
+ reset: '\x1b[0m',
8
+ bold: '\x1b[1m',
9
+ dim: '\x1b[2m',
10
+ cyan: '\x1b[36m',
11
+ green: '\x1b[32m',
12
+ yellow: '\x1b[33m',
13
+ red: '\x1b[31m',
14
+ };
15
+
16
+ function colorize(text, color, enabled) {
17
+ return enabled ? `${color}${text}${ANSI.reset}` : text;
18
+ }
19
+
20
+ export function readJson(rootDir, relativePath) {
21
+ const target = join(rootDir, relativePath);
22
+ if (!existsSync(target)) {
23
+ return null;
24
+ }
25
+
26
+ return JSON.parse(readFileSync(target, 'utf8'));
27
+ }
28
+
29
+ export function readText(rootDir, relativePath) {
30
+ const target = join(rootDir, relativePath);
31
+ if (!existsSync(target)) {
32
+ return null;
33
+ }
34
+
35
+ return readFileSync(target, 'utf8').trim();
36
+ }
37
+
38
+ export function getMenuStatus(rootDir) {
39
+ const packageJson = readJson(rootDir, 'package.json');
40
+ const stableManifest = readJson(rootDir, 'manifests/channels/stable.json');
41
+ const versionText = readText(rootDir, 'VERSION.md');
42
+
43
+ return {
44
+ packageName: packageJson?.name ?? 'nao encontrado',
45
+ semver: packageJson?.version ?? 'nao encontrado',
46
+ manifestVersion: stableManifest?.version ?? 'nao encontrado',
47
+ manifestSemver: stableManifest?.semver ?? 'nao encontrado',
48
+ versionText,
49
+ rootDir,
50
+ };
51
+ }
52
+
53
+ export function buildMenuCommands({ rootDir, currentFile, nodeExecPath = process.execPath }) {
54
+ return [
55
+ {
56
+ key: 'status',
57
+ aliases: ['status'],
58
+ label: 'Status local',
59
+ description: 'Mostra versao local, manifesto e informacoes do pacote.',
60
+ command: nodeExecPath,
61
+ args: [currentFile, '--status'],
62
+ },
63
+ {
64
+ key: 'check',
65
+ aliases: ['check', 'gate'],
66
+ label: 'Rodar gate completo',
67
+ description: 'Executa build, testes e validacao de manifestos.',
68
+ command: 'npm',
69
+ args: ['run', 'check'],
70
+ },
71
+ {
72
+ key: 'build',
73
+ aliases: ['build'],
74
+ label: 'Rodar build',
75
+ description: 'Compila o projeto localmente.',
76
+ command: 'npm',
77
+ args: ['run', 'build'],
78
+ },
79
+ {
80
+ key: 'publicNpm',
81
+ aliases: ['public-npm', 'npm', 'registry'],
82
+ label: 'Validar pacote no npm publico',
83
+ description: 'Consulta a versao publicada em registry.npmjs.org.',
84
+ command: 'npm',
85
+ args: [
86
+ 'view',
87
+ '@fprad0/skill-master-mcp',
88
+ 'version',
89
+ '--registry=https://registry.npmjs.org',
90
+ ],
91
+ },
92
+ {
93
+ key: 'updateGlobal',
94
+ aliases: ['update', 'update-global'],
95
+ label: 'Atualizar pacote global via npm',
96
+ description: 'Atualiza a instalacao global e exige reinicio do cliente MCP.',
97
+ command: nodeExecPath,
98
+ args: [join(rootDir, 'bin', 'skill-master-update.mjs')],
99
+ confirmMessage: 'Atualizar o pacote global agora?',
100
+ },
101
+ {
102
+ key: 'privateRegistry',
103
+ aliases: ['private-registry', 'private', 'github-packages'],
104
+ label: 'Configurar registry privado GitHub Packages',
105
+ description: 'Prepara o .npmrc e valida o acesso ao pacote privado.',
106
+ command: nodeExecPath,
107
+ args: ['scripts/configure-private-registry.mjs', '--validate'],
108
+ confirmMessage: 'Rodar a configuracao de registry privado agora?',
109
+ },
110
+ ];
111
+ }
112
+
113
+ export function buildMenuChoices(commands) {
114
+ return commands.map((command) => ({
115
+ title: command.label,
116
+ description: command.description,
117
+ value: command.key,
118
+ })).concat({
119
+ title: 'Sair',
120
+ description: 'Fecha o menu operacional.',
121
+ value: '__exit__',
122
+ });
123
+ }
124
+
125
+ export function resolveActionKey(input, commands) {
126
+ const normalized = (input ?? '').trim().toLowerCase();
127
+ if (!normalized) {
128
+ return null;
129
+ }
130
+
131
+ return commands.find((command) =>
132
+ command.key.toLowerCase() === normalized || command.aliases.includes(normalized),
133
+ )?.key ?? null;
134
+ }
135
+
136
+ export function isInteractiveTerminal() {
137
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
138
+ }
139
+
140
+ export function formatStatusReport(status) {
141
+ const lines = [
142
+ 'Skill Master MCP - status local',
143
+ `Diretorio: ${status.rootDir}`,
144
+ `Pacote: ${status.packageName}`,
145
+ `Semver local: ${status.semver}`,
146
+ `Manifesto stable: ${status.manifestVersion}`,
147
+ `Manifesto semver: ${status.manifestSemver}`,
148
+ ];
149
+
150
+ if (status.versionText) {
151
+ lines.push('', 'VERSION.md:');
152
+ lines.push(...status.versionText.split('\n').slice(0, 6));
153
+ }
154
+
155
+ return lines.join('\n');
156
+ }
157
+
158
+ function renderPanelLines(lines, { color = ANSI.cyan, useColor = false } = {}) {
159
+ const width = Math.max(...lines.map((line) => line.length), 24);
160
+ const border = `+${'-'.repeat(width + 2)}+`;
161
+ const paintedBorder = colorize(border, color, useColor);
162
+ const body = lines.map((line) => `| ${line.padEnd(width)} |`);
163
+ return [paintedBorder, ...body, paintedBorder].join('\n');
164
+ }
165
+
166
+ export function formatMenuBanner(status, { useColor = false } = {}) {
167
+ const lines = [
168
+ colorize('Skill Master MCP', ANSI.bold, useColor),
169
+ 'Menu operacional para manutencao local',
170
+ `Versao local ${status.semver} | canal ${status.manifestVersion}`,
171
+ colorize('Setas + Enter para navegar', ANSI.dim, useColor),
172
+ ];
173
+ return renderPanelLines(lines, { color: ANSI.cyan, useColor });
174
+ }
175
+
176
+ export function formatActionHeader(action, { useColor = false } = {}) {
177
+ return renderPanelLines(
178
+ [
179
+ colorize(action.label, ANSI.bold, useColor),
180
+ action.description,
181
+ ],
182
+ { color: ANSI.green, useColor },
183
+ );
184
+ }
185
+
186
+ export function formatResultMessage(code, { useColor = false } = {}) {
187
+ if (code === 0) {
188
+ return colorize('Comando concluido com sucesso.', ANSI.green, useColor);
189
+ }
190
+
191
+ return colorize(`Comando terminou com codigo ${code}.`, ANSI.red, useColor);
192
+ }
193
+
194
+ export function formatHelp(commands) {
195
+ const actionList = commands.map((command) => ` - ${command.key}: ${command.label}`).join('\n');
196
+
197
+ return `Skill Master Menu
198
+
199
+ Uso:
200
+ skill-master-menu
201
+ skill-master-menu --status
202
+ skill-master-menu --run <acao>
203
+ skill-master-menu --run update --yes
204
+ skill-master-menu --help
205
+
206
+ Acoes disponiveis:
207
+ ${actionList}
208
+
209
+ O comando de menu e voltado para operacao humana. O binario MCP stdio continua sendo:
210
+ skill-master-mcp
211
+ `;
212
+ }
213
+
214
+ export function runCommand(action, { cwd, env = process.env } = {}) {
215
+ return new Promise((resolve) => {
216
+ const child = spawn(action.command, action.args, {
217
+ cwd,
218
+ env,
219
+ shell: process.platform === 'win32',
220
+ stdio: 'inherit',
221
+ });
222
+
223
+ child.on('close', (code) => {
224
+ resolve(code ?? 1);
225
+ });
226
+ });
227
+ }
@@ -1,179 +1,172 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { spawn } from 'node:child_process';
4
- import { existsSync, readFileSync } from 'node:fs';
5
- import { dirname, join } from 'node:path';
3
+ import prompts from 'prompts';
4
+ import { dirname } from 'node:path';
6
5
  import { fileURLToPath } from 'node:url';
7
- import { createInterface } from 'node:readline/promises';
8
- import { stdin as input, stdout as output } from 'node:process';
9
-
10
- const rootDir = dirname(dirname(fileURLToPath(import.meta.url)));
11
-
12
- const commands = {
13
- status: {
14
- label: 'Mostrar status local',
15
- command: process.execPath,
16
- args: [fileURLToPath(import.meta.url), '--status'],
17
- },
18
- check: {
19
- label: 'Rodar gate completo',
20
- command: 'npm',
21
- args: ['run', 'check'],
22
- },
23
- build: {
24
- label: 'Rodar build',
25
- command: 'npm',
26
- args: ['run', 'build'],
27
- },
28
- publicNpm: {
29
- label: 'Validar pacote no npm publico',
30
- command: 'npm',
31
- args: [
32
- 'view',
33
- '@fprad0/skill-master-mcp',
34
- 'version',
35
- '--registry=https://registry.npmjs.org',
36
- ],
37
- },
38
- updateGlobal: {
39
- label: 'Atualizar pacote global via npm',
40
- command: process.execPath,
41
- args: [join(rootDir, 'bin', 'skill-master-update.mjs')],
42
- },
43
- privateRegistry: {
44
- label: 'Configurar registry privado GitHub Packages',
45
- command: process.execPath,
46
- args: ['scripts/configure-private-registry.mjs', '--validate'],
47
- },
48
- };
49
-
50
- const menuItems = [
51
- ['1', commands.status],
52
- ['2', commands.check],
53
- ['3', commands.build],
54
- ['4', commands.publicNpm],
55
- ['5', commands.updateGlobal],
56
- ['6', commands.privateRegistry],
57
- ];
58
-
59
- function readJson(relativePath) {
60
- const target = join(rootDir, relativePath);
61
- if (!existsSync(target)) {
62
- return null;
6
+ import {
7
+ buildMenuChoices,
8
+ buildMenuCommands,
9
+ formatActionHeader,
10
+ formatHelp,
11
+ formatMenuBanner,
12
+ formatResultMessage,
13
+ formatStatusReport,
14
+ getMenuStatus,
15
+ isInteractiveTerminal,
16
+ resolveActionKey,
17
+ runCommand,
18
+ } from './lib/menu-core.mjs';
19
+
20
+ const currentFile = fileURLToPath(import.meta.url);
21
+ const rootDir = dirname(dirname(currentFile));
22
+ const commands = buildMenuCommands({ rootDir, currentFile });
23
+
24
+ function parseArgs(argv) {
25
+ const parsed = {
26
+ help: false,
27
+ status: false,
28
+ yes: false,
29
+ run: null,
30
+ };
31
+
32
+ for (let i = 0; i < argv.length; i += 1) {
33
+ const arg = argv[i];
34
+ switch (arg) {
35
+ case '--help':
36
+ case '-h':
37
+ parsed.help = true;
38
+ break;
39
+ case '--status':
40
+ parsed.status = true;
41
+ break;
42
+ case '--yes':
43
+ parsed.yes = true;
44
+ break;
45
+ case '--run':
46
+ parsed.run = argv[++i] ?? null;
47
+ break;
48
+ default:
49
+ throw new Error(`Unknown argument: ${arg}`);
50
+ }
63
51
  }
64
52
 
65
- return JSON.parse(readFileSync(target, 'utf8'));
53
+ return parsed;
66
54
  }
67
55
 
68
- function readText(relativePath) {
69
- const target = join(rootDir, relativePath);
70
- if (!existsSync(target)) {
71
- return null;
72
- }
56
+ function printHelp() {
57
+ console.log(formatHelp(commands));
58
+ }
73
59
 
74
- return readFileSync(target, 'utf8').trim();
60
+ function printStatus() {
61
+ console.log(formatStatusReport(getMenuStatus(rootDir)));
75
62
  }
76
63
 
77
- function printHelp() {
78
- console.log(`Skill Master Menu
64
+ async function runSelectedAction(action, { yes = false, useColor = false } = {}) {
65
+ if (action.confirmMessage && !yes) {
66
+ if (!isInteractiveTerminal()) {
67
+ throw new Error(`Action ${action.key} requires --yes outside an interactive terminal.`);
68
+ }
79
69
 
80
- Uso:
81
- skill-master-menu
82
- skill-master-menu --status
83
- skill-master-menu --help
70
+ const confirmation = await prompts({
71
+ type: 'confirm',
72
+ name: 'confirmed',
73
+ message: action.confirmMessage,
74
+ initial: false,
75
+ });
84
76
 
85
- Opcao 5 do menu:
86
- atualiza o pacote global com npm install -g @fprad0/skill-master-mcp@latest
77
+ if (!confirmation.confirmed) {
78
+ return { cancelled: true, code: 0 };
79
+ }
80
+ }
87
81
 
88
- O comando de menu e voltado para operacao humana. O binario MCP stdio continua sendo:
89
- skill-master-mcp
90
- `);
82
+ console.log('');
83
+ console.log(formatActionHeader(action, { useColor }));
84
+ console.log('');
85
+ const code = await runCommand(action, { cwd: rootDir });
86
+ console.log('');
87
+ console.log(formatResultMessage(code, { useColor }));
88
+ return { cancelled: false, code };
91
89
  }
92
90
 
93
- function printStatus() {
94
- const packageJson = readJson('package.json');
95
- const stableManifest = readJson('manifests/channels/stable.json');
96
- const versionText = readText('VERSION.md');
97
-
98
- console.log('Skill Master MCP - status local');
99
- console.log(`Diretorio: ${rootDir}`);
100
- console.log(`Pacote: ${packageJson?.name ?? 'nao encontrado'}`);
101
- console.log(`Semver local: ${packageJson?.version ?? 'nao encontrado'}`);
102
- console.log(`Manifesto stable: ${stableManifest?.version ?? 'nao encontrado'}`);
103
- console.log(`Manifesto semver: ${stableManifest?.semver ?? 'nao encontrado'}`);
104
-
105
- if (versionText) {
106
- const firstLines = versionText.split('\n').slice(0, 6).join('\n');
107
- console.log('\nVERSION.md:');
108
- console.log(firstLines);
109
- }
110
- }
91
+ async function runVisualMenu() {
92
+ while (true) {
93
+ console.clear();
94
+ const status = getMenuStatus(rootDir);
95
+ console.log(formatMenuBanner(status, { useColor: true }));
96
+ console.log('');
97
+
98
+ const selection = await prompts({
99
+ type: 'select',
100
+ name: 'action',
101
+ message: 'Escolha uma acao',
102
+ choices: buildMenuChoices(commands),
103
+ initial: 0,
104
+ });
111
105
 
112
- function runCommand(commandConfig) {
113
- return new Promise((resolve) => {
114
- console.log(`\n> ${commandConfig.command} ${commandConfig.args.join(' ')}\n`);
106
+ if (!selection.action || selection.action === '__exit__') {
107
+ return 0;
108
+ }
115
109
 
116
- const child = spawn(commandConfig.command, commandConfig.args, {
117
- cwd: rootDir,
118
- env: process.env,
119
- shell: process.platform === 'win32',
120
- stdio: 'inherit',
121
- });
110
+ const action = commands.find((entry) => entry.key === selection.action);
111
+ if (!action) {
112
+ return 1;
113
+ }
122
114
 
123
- child.on('close', (code) => {
124
- resolve(code ?? 1);
115
+ const result = await runSelectedAction(action, { useColor: true });
116
+ if (result.cancelled) {
117
+ continue;
118
+ }
119
+
120
+ const nextStep = await prompts({
121
+ type: 'toggle',
122
+ name: 'keepGoing',
123
+ message: 'Voltar ao menu?',
124
+ initial: true,
125
+ active: 'sim',
126
+ inactive: 'nao',
125
127
  });
126
- });
127
- }
128
128
 
129
- async function runInteractiveMenu() {
130
- const rl = createInterface({ input, output });
131
-
132
- try {
133
- while (true) {
134
- console.log('\nSkill Master Menu');
135
- for (const [key, item] of menuItems) {
136
- console.log(` ${key}. ${item.label}`);
137
- }
138
- console.log(' 0. Sair');
139
-
140
- const answer = (await rl.question('\nEscolha uma opcao: ')).trim();
141
-
142
- if (answer === '0' || answer.toLowerCase() === 'q') {
143
- return 0;
144
- }
145
-
146
- const selected = menuItems.find(([key]) => key === answer)?.[1];
147
- if (!selected) {
148
- console.log('Opcao invalida.');
149
- continue;
150
- }
151
-
152
- const code = await runCommand(selected);
153
- if (code !== 0) {
154
- console.log(`\nComando terminou com codigo ${code}.`);
155
- }
129
+ if (!nextStep.keepGoing) {
130
+ return result.code;
156
131
  }
157
- } finally {
158
- rl.close();
159
132
  }
160
133
  }
161
134
 
162
135
  async function main() {
163
- const args = process.argv.slice(2);
136
+ const args = parseArgs(process.argv.slice(2));
164
137
 
165
- if (args.includes('--help') || args.includes('-h')) {
138
+ if (args.help) {
166
139
  printHelp();
167
140
  return 0;
168
141
  }
169
142
 
170
- if (args.includes('--status')) {
143
+ if (args.status) {
171
144
  printStatus();
172
145
  return 0;
173
146
  }
174
147
 
175
- return runInteractiveMenu();
148
+ if (args.run) {
149
+ const actionKey = resolveActionKey(args.run, commands);
150
+ if (!actionKey) {
151
+ throw new Error(`Unknown action: ${args.run}`);
152
+ }
153
+
154
+ const action = commands.find((entry) => entry.key === actionKey);
155
+ const result = await runSelectedAction(action, { yes: args.yes, useColor: isInteractiveTerminal() });
156
+ return result.code;
157
+ }
158
+
159
+ if (!isInteractiveTerminal()) {
160
+ throw new Error('Interactive menu requires a TTY. Use --run <acao> or --status in automated environments.');
161
+ }
162
+
163
+ return runVisualMenu();
176
164
  }
177
165
 
178
- const exitCode = await main();
179
- process.exitCode = exitCode;
166
+ try {
167
+ process.exitCode = await main();
168
+ } catch (error) {
169
+ const message = error instanceof Error ? error.message : String(error);
170
+ process.stderr.write(`[skill_master] ${message}\n`);
171
+ process.exitCode = 1;
172
+ }
@@ -11,6 +11,23 @@ O fluxo usa:
11
11
  - execucao via `npx @fprad0/skill-master-mcp`;
12
12
  - configuracao MCP por `stdio`.
13
13
 
14
+ ## Modelo de seguranca do `.npmrc`
15
+
16
+ O configurador segue tres regras:
17
+
18
+ - o token fica escopado ao host do registry, por exemplo `//npm.pkg.github.com/:_authToken=...`;
19
+ - o registry precisa usar HTTPS por padrao;
20
+ - o arquivo `.npmrc` usa referencia de variavel de ambiente por padrao, evitando gravar o token literal.
21
+
22
+ Conteudo recomendado:
23
+
24
+ ```ini
25
+ @fprad0:registry=https://npm.pkg.github.com
26
+ //npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_TOKEN}
27
+ ```
28
+
29
+ Se voce precisar persistir o token literal dentro do `.npmrc`, use `--store-token` de forma consciente. Esse modo e menos seguro, mas pode ser necessario em maquinas sem suporte simples a variaveis de ambiente persistentes.
30
+
14
31
  ## Pre-requisitos
15
32
 
16
33
  - Node.js e npm instalados;
@@ -65,7 +82,8 @@ O script:
65
82
 
66
83
  - pede o token de forma segura se ele no estiver em variavel de ambiente;
67
84
  - atualiza apenas as linhas necessrias do `%USERPROFILE%\.npmrc`;
68
- - cria backup em `%USERPROFILE%\.npmrc.bak` se o arquivo j existir;
85
+ - cria backup com timestamp se o arquivo ja existir;
86
+ - grava o `.npmrc` apontando para `${GITHUB_PACKAGES_TOKEN}` por padrao;
69
87
  - valida acesso ao pacote com `npm view`;
70
88
  - mostra o comando final de `npx`.
71
89
 
@@ -75,11 +93,17 @@ Crie ou atualize o arquivo:
75
93
  %USERPROFILE%\.npmrc
76
94
  ```
77
95
 
78
- Com o conteudo:
96
+ Com o conteudo recomendado:
79
97
 
80
98
  ```ini
81
99
  @fprad0:registry=https://npm.pkg.github.com
82
- //npm.pkg.github.com/:_authToken=SEU_TOKEN_AQUI
100
+ //npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_TOKEN}
101
+ ```
102
+
103
+ Se preferir gravar o token literal no `.npmrc`:
104
+
105
+ ```powershell
106
+ powershell -ExecutionPolicy Bypass -File .\scripts\configure-private-registry.ps1 -Validate -StoreToken
83
107
  ```
84
108
 
85
109
  ### Jeito rapido sem checkout local
@@ -91,6 +115,12 @@ $env:GITHUB_PACKAGES_TOKEN='SEU_TOKEN'
91
115
  npx -y -p @fprad0/skill-master-mcp skill-master-configure-private-registry --validate
92
116
  ```
93
117
 
118
+ Para ler o token de arquivo, evitando argumento em historico de shell:
119
+
120
+ ```powershell
121
+ npx -y -p @fprad0/skill-master-mcp skill-master-configure-private-registry --token-file .\github-packages-token.txt --validate
122
+ ```
123
+
94
124
  ## Validar acesso ao pacote
95
125
 
96
126
  ```bat
@@ -112,6 +142,8 @@ O script:
112
142
  - pede o token sem eco no terminal quando necessario;
113
143
  - atualiza apenas as linhas do escopo `@fprad0`;
114
144
  - cria ou reaproveita `${HOME}/.npmrc`;
145
+ - aplica permissao `0600` no `${HOME}/.npmrc`;
146
+ - grava o `.npmrc` apontando para `${GITHUB_PACKAGES_TOKEN}` por padrao;
115
147
  - valida acesso ao pacote;
116
148
  - mostra o comando final de `npx`.
117
149
 
@@ -123,6 +155,18 @@ Use esta opcao quando a maquina ja consegue resolver o escopo `@fprad0` no GitHu
123
155
  GITHUB_PACKAGES_TOKEN='SEU_TOKEN' npx -y -p @fprad0/skill-master-mcp skill-master-configure-private-registry --validate
124
156
  ```
125
157
 
158
+ Para ler o token via stdin:
159
+
160
+ ```bash
161
+ printf '%s' "$GITHUB_PACKAGES_TOKEN" | npx -y -p @fprad0/skill-master-mcp skill-master-configure-private-registry --token-stdin --validate
162
+ ```
163
+
164
+ Para gravar token literal no `.npmrc` quando necessario:
165
+
166
+ ```bash
167
+ bash ./scripts/configure-private-registry.sh --validate --store-token
168
+ ```
169
+
126
170
  ## Publicacao pelo workflow
127
171
 
128
172
  O workflow de release usa:
@@ -2,12 +2,12 @@
2
2
  "name": "skill_master",
3
3
  "channel": "beta",
4
4
  "version": "00.02-beta",
5
- "semver": "0.0.5",
6
- "publishedAt": "2026-06-26T14:35:00-03:00",
5
+ "semver": "0.0.7",
6
+ "publishedAt": "2026-06-26T20:15:00-03:00",
7
7
  "git": {
8
8
  "repo": "https://github.com/FPrad0/skill-master-mcp",
9
9
  "branch": "main",
10
- "commit": "af41244"
10
+ "commit": "ec57c95"
11
11
  },
12
12
  "node": {
13
13
  "minimum": "18.0.0"
@@ -19,7 +19,7 @@
19
19
  "buildCommand": "npm run build"
20
20
  },
21
21
  "changelog": [
22
- "Canal beta acompanha o novo comando skill-master-update.",
23
- "Update npm fica disponivel via comando separado e pelo menu operacional."
22
+ "Canal beta acompanha o menu visual no terminal.",
23
+ "Fluxo interativo agora tem selecao visual, confirmacao e modo direto por acao."
24
24
  ]
25
25
  }
@@ -2,12 +2,12 @@
2
2
  "name": "skill_master",
3
3
  "channel": "stable",
4
4
  "version": "00.02",
5
- "semver": "0.0.5",
6
- "publishedAt": "2026-06-26T14:35:00-03:00",
5
+ "semver": "0.0.7",
6
+ "publishedAt": "2026-06-26T20:15:00-03:00",
7
7
  "git": {
8
8
  "repo": "https://github.com/FPrad0/skill-master-mcp",
9
9
  "branch": "main",
10
- "commit": "af41244"
10
+ "commit": "ec57c95"
11
11
  },
12
12
  "node": {
13
13
  "minimum": "18.0.0"
@@ -19,8 +19,8 @@
19
19
  "buildCommand": "npm run build"
20
20
  },
21
21
  "changelog": [
22
- "Novo binario skill-master-update para atualizar instalacao global via npm.",
23
- "Menu operacional passa a incluir opcao de update global seguro.",
24
- "Fluxo evita atualizar arquivos durante execucao do servidor MCP stdio."
22
+ "Menu operacional ganha interface visual no terminal com prompts.",
23
+ "Core do menu foi extraido para modulo reutilizavel e testavel.",
24
+ "Compatibilidade Node 18+ foi preservada sem migrar para TUI pesada."
25
25
  ]
26
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fprad0/skill-master-mcp",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Personal MCP server that catalogs, recommends and reports skills with update-aware metadata.",
@@ -51,6 +51,7 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@modelcontextprotocol/sdk": "^1.29.0",
54
+ "prompts": "^2.4.2",
54
55
  "zod": "^4.4.3"
55
56
  },
56
57
  "devDependencies": {
@@ -1,14 +1,39 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
3
  import { dirname, resolve } from 'node:path';
4
4
  import {
5
5
  DEFAULT_PACKAGE_NAME,
6
6
  DEFAULT_REGISTRY_URL,
7
7
  DEFAULT_SCOPE,
8
8
  DEFAULT_TOKEN_ENV,
9
+ redactSensitiveNpmrc,
9
10
  upsertNpmrc,
10
11
  } from './lib/private-registry.mjs';
11
12
 
13
+ function printHelp() {
14
+ process.stdout.write(`Skill Master private registry configurator
15
+
16
+ Usage:
17
+ skill-master-configure-private-registry [options]
18
+
19
+ Options:
20
+ --npmrc <path> Target .npmrc path. Defaults to the user .npmrc.
21
+ --scope <scope> npm scope. Defaults to @fprad0.
22
+ --registry <url> Registry URL. Defaults to https://npm.pkg.github.com.
23
+ --token <token> Token for validation. Avoid in shared shell history.
24
+ --token-file <path> Read token from a local file.
25
+ --token-stdin Read token from stdin.
26
+ --token-env <name> Environment variable name. Defaults to GITHUB_PACKAGES_TOKEN.
27
+ --store-token Persist the token literal in .npmrc. Default stores \${TOKEN_ENV}.
28
+ --allow-insecure-registry Allow http only for localhost/private test registries.
29
+ --no-backup Do not create a timestamped backup.
30
+ --dry-run Print a redacted preview without writing.
31
+ --validate Run npm view after writing.
32
+ --package <name> Package used for validation.
33
+ --help Show this help.
34
+ `);
35
+ }
36
+
12
37
  function parseArgs(argv) {
13
38
  const parsed = {
14
39
  npmrc: resolve(process.env.USERPROFILE ?? process.env.HOME ?? '.', '.npmrc'),
@@ -19,6 +44,9 @@ function parseArgs(argv) {
19
44
  dryRun: false,
20
45
  validate: false,
21
46
  packageName: DEFAULT_PACKAGE_NAME,
47
+ storeToken: false,
48
+ allowInsecureRegistry: false,
49
+ help: false,
22
50
  };
23
51
 
24
52
  for (let i = 0; i < argv.length; i += 1) {
@@ -36,9 +64,21 @@ function parseArgs(argv) {
36
64
  case '--token':
37
65
  parsed.token = argv[++i];
38
66
  break;
67
+ case '--token-file':
68
+ parsed.tokenFile = resolve(argv[++i]);
69
+ break;
70
+ case '--token-stdin':
71
+ parsed.tokenStdin = true;
72
+ break;
39
73
  case '--token-env':
40
74
  parsed.tokenEnv = argv[++i];
41
75
  break;
76
+ case '--store-token':
77
+ parsed.storeToken = true;
78
+ break;
79
+ case '--allow-insecure-registry':
80
+ parsed.allowInsecureRegistry = true;
81
+ break;
42
82
  case '--no-backup':
43
83
  parsed.backup = false;
44
84
  break;
@@ -51,6 +91,10 @@ function parseArgs(argv) {
51
91
  case '--package':
52
92
  parsed.packageName = argv[++i];
53
93
  break;
94
+ case '--help':
95
+ case '-h':
96
+ parsed.help = true;
97
+ break;
54
98
  default:
55
99
  throw new Error(`Unknown argument: ${arg}`);
56
100
  }
@@ -59,12 +103,39 @@ function parseArgs(argv) {
59
103
  return parsed;
60
104
  }
61
105
 
106
+ function readToken(options) {
107
+ if (options.token) {
108
+ return options.token;
109
+ }
110
+
111
+ if (options.tokenFile) {
112
+ return readFileSync(options.tokenFile, 'utf8').trim();
113
+ }
114
+
115
+ if (options.tokenStdin) {
116
+ return readFileSync(0, 'utf8').trim();
117
+ }
118
+
119
+ return process.env[options.tokenEnv];
120
+ }
121
+
122
+ function buildBackupPath(npmrcPath) {
123
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
124
+ return `${npmrcPath}.bak.${timestamp}`;
125
+ }
126
+
62
127
  function main() {
63
128
  const options = parseArgs(process.argv.slice(2));
64
- const token = options.token ?? process.env[options.tokenEnv];
65
129
 
66
- if (!token) {
67
- throw new Error(`Token not provided. Use --token or define ${options.tokenEnv}.`);
130
+ if (options.help) {
131
+ printHelp();
132
+ return;
133
+ }
134
+
135
+ const token = readToken(options);
136
+ const needsToken = options.storeToken || options.validate;
137
+ if (needsToken && !token) {
138
+ throw new Error(`Token not provided. Use --token-file, --token-stdin, --token, or define ${options.tokenEnv}.`);
68
139
  }
69
140
 
70
141
  const currentContent = existsSync(options.npmrc)
@@ -75,16 +146,24 @@ function main() {
75
146
  scope: options.scope,
76
147
  registryUrl: options.registry,
77
148
  tokenValue: token,
149
+ tokenEnv: options.tokenEnv,
150
+ storeToken: options.storeToken,
151
+ allowInsecureRegistry: options.allowInsecureRegistry,
78
152
  });
79
153
 
80
154
  const changed = nextContent !== currentContent.replace(/\r\n/g, '\n');
81
155
 
82
- if (!options.dryRun) {
156
+ if (options.dryRun) {
157
+ process.stdout.write(redactSensitiveNpmrc(nextContent));
158
+ } else {
83
159
  mkdirSync(dirname(options.npmrc), { recursive: true });
84
160
  if (changed && options.backup && existsSync(options.npmrc)) {
85
- copyFileSync(options.npmrc, `${options.npmrc}.bak`);
161
+ copyFileSync(options.npmrc, buildBackupPath(options.npmrc));
86
162
  }
87
163
  writeFileSync(options.npmrc, nextContent, 'utf8');
164
+ if (process.platform !== 'win32') {
165
+ chmodSync(options.npmrc, 0o600);
166
+ }
88
167
  }
89
168
 
90
169
  if (options.validate) {
@@ -95,7 +174,11 @@ function main() {
95
174
  const result = spawnSync(
96
175
  'npm',
97
176
  ['view', options.packageName, `--registry=${options.registry}`],
98
- { stdio: 'inherit', shell: process.platform === 'win32' },
177
+ {
178
+ env: { ...process.env, [options.tokenEnv]: token },
179
+ stdio: 'inherit',
180
+ shell: process.platform === 'win32',
181
+ },
99
182
  );
100
183
 
101
184
  if (result.status !== 0) {
@@ -108,6 +191,7 @@ function main() {
108
191
  `[skill_master] ${options.dryRun ? 'Preview ready for' : 'Configured'} ${options.npmrc}`,
109
192
  `[skill_master] Scope: ${options.scope}`,
110
193
  `[skill_master] Registry: ${options.registry}`,
194
+ `[skill_master] Token storage: ${options.storeToken ? 'literal token in .npmrc' : `environment reference \${${options.tokenEnv}}`}`,
111
195
  `[skill_master] Changed: ${changed ? 'yes' : 'no'}`,
112
196
  `[skill_master] Validate with: npm view ${options.packageName} --registry=${options.registry}`,
113
197
  `[skill_master] Run with: npx -y ${options.packageName}@latest`,
@@ -3,30 +3,68 @@ export const DEFAULT_PACKAGE_NAME = '@fprad0/skill-master-mcp';
3
3
  export const DEFAULT_REGISTRY_URL = 'https://npm.pkg.github.com';
4
4
  export const DEFAULT_TOKEN_ENV = 'GITHUB_PACKAGES_TOKEN';
5
5
 
6
- export function registryHostFromUrl(registryUrl = DEFAULT_REGISTRY_URL) {
7
- return new URL(registryUrl).host;
6
+ export function normalizeRegistryUrl(registryUrl = DEFAULT_REGISTRY_URL, { allowInsecure = false } = {}) {
7
+ const parsed = new URL(registryUrl);
8
+ const isLocalhost = ['localhost', '127.0.0.1', '::1'].includes(parsed.hostname);
9
+
10
+ if (parsed.protocol !== 'https:' && !(allowInsecure && isLocalhost)) {
11
+ throw new Error(`Registry must use HTTPS: ${registryUrl}`);
12
+ }
13
+
14
+ parsed.hash = '';
15
+ parsed.search = '';
16
+ return parsed.toString().replace(/\/$/, '');
17
+ }
18
+
19
+ export function registryHostFromUrl(registryUrl = DEFAULT_REGISTRY_URL, { allowInsecure = false } = {}) {
20
+ return new URL(normalizeRegistryUrl(registryUrl, { allowInsecure })).host;
8
21
  }
9
22
 
10
23
  export function buildRegistryLine(scope = DEFAULT_SCOPE, registryUrl = DEFAULT_REGISTRY_URL) {
11
- return `${scope}:registry=${registryUrl}`;
24
+ return `${scope}:registry=${normalizeRegistryUrl(registryUrl)}`;
12
25
  }
13
26
 
14
27
  export function buildTokenLine(registryUrl = DEFAULT_REGISTRY_URL, tokenValue) {
15
28
  return `//${registryHostFromUrl(registryUrl)}/:_authToken=${tokenValue}`;
16
29
  }
17
30
 
31
+ export function buildTokenEnvReference(tokenEnv = DEFAULT_TOKEN_ENV) {
32
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tokenEnv)) {
33
+ throw new Error(`Invalid token environment variable name: ${tokenEnv}`);
34
+ }
35
+
36
+ return '${' + tokenEnv + '}';
37
+ }
38
+
39
+ export function validateScope(scope = DEFAULT_SCOPE) {
40
+ if (!/^@[a-z0-9][a-z0-9._-]*$/i.test(scope)) {
41
+ throw new Error(`Invalid npm scope: ${scope}`);
42
+ }
43
+
44
+ return scope;
45
+ }
46
+
18
47
  export function upsertNpmrc(content, {
19
48
  scope = DEFAULT_SCOPE,
20
49
  registryUrl = DEFAULT_REGISTRY_URL,
21
50
  tokenValue,
51
+ tokenEnv = DEFAULT_TOKEN_ENV,
52
+ storeToken = false,
53
+ allowInsecureRegistry = false,
22
54
  } = {}) {
23
- if (!tokenValue) {
24
- throw new Error('tokenValue is required');
55
+ const normalizedScope = validateScope(scope);
56
+ const normalizedRegistryUrl = normalizeRegistryUrl(registryUrl, { allowInsecure: allowInsecureRegistry });
57
+ const tokenValueForFile = storeToken
58
+ ? tokenValue
59
+ : buildTokenEnvReference(tokenEnv);
60
+
61
+ if (!tokenValueForFile) {
62
+ throw new Error(storeToken ? 'tokenValue is required when storeToken is enabled' : 'tokenEnv is required');
25
63
  }
26
64
 
27
65
  const existing = (content ?? '').replace(/\r\n/g, '\n');
28
- const registryLinePrefix = `${scope}:registry=`;
29
- const tokenLinePrefix = `//${registryHostFromUrl(registryUrl)}/:_authToken=`;
66
+ const registryLinePrefix = `${normalizedScope}:registry=`;
67
+ const tokenLinePrefix = `//${registryHostFromUrl(normalizedRegistryUrl, { allowInsecure: allowInsecureRegistry })}/:_authToken=`;
30
68
 
31
69
  const filteredLines = existing
32
70
  .split('\n')
@@ -40,8 +78,8 @@ export function upsertNpmrc(content, {
40
78
  filteredLines.push('');
41
79
  }
42
80
 
43
- filteredLines.push(buildRegistryLine(scope, registryUrl));
44
- filteredLines.push(buildTokenLine(registryUrl, tokenValue));
81
+ filteredLines.push(buildRegistryLine(normalizedScope, normalizedRegistryUrl));
82
+ filteredLines.push(buildTokenLine(normalizedRegistryUrl, tokenValueForFile));
45
83
 
46
84
  return `${filteredLines.join('\n')}\n`;
47
85
  }
@@ -54,3 +92,6 @@ export function hasRegistryConfig(content, {
54
92
  return normalized.includes(buildRegistryLine(scope, registryUrl));
55
93
  }
56
94
 
95
+ export function redactSensitiveNpmrc(content) {
96
+ return (content ?? '').replace(/(:_authToken=)(.+)$/gm, '$1<redacted>');
97
+ }