@arzstack/hub 0.1.0
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 +52 -0
- package/package.json +15 -0
- package/src/index.mjs +186 -0
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# @arzstack/hub — instalador
|
|
2
|
+
|
|
3
|
+
CLI que instala itens do **ArzStack Hub** (skills, MCPs…) com um comando só,
|
|
4
|
+
type-aware e com escolha de destino. Scaffold — **a publicar no npm**.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npx @arzstack/hub install <token> --global # ou --project
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
O `<token>` é gerado na página do item no Hub (botão "Instalar" → copiar). É curto,
|
|
11
|
+
assinado e escopado a 1 item + 1 org. O CLI:
|
|
12
|
+
|
|
13
|
+
1. lê o `hub` + `item` do token;
|
|
14
|
+
2. busca o manifesto em `GET {hub}/api/install/manifest?token=…`;
|
|
15
|
+
3. instala conforme o tipo:
|
|
16
|
+
- **skill** → `git clone` em `~/.agents/skills/<slug>` (`--global`) ou `.agents/skills/<slug>` (`--project`);
|
|
17
|
+
- **mcp_server** → roda o comando de configuração (ex.: `claude mcp add …`);
|
|
18
|
+
- outros → mostra repositório/passos.
|
|
19
|
+
4. confirma antes de executar (`--yes` pula); reporta o uso pro Hub (analytics).
|
|
20
|
+
|
|
21
|
+
Flags: `--global` | `--project` | `--dir <path>` | `--yes`.
|
|
22
|
+
|
|
23
|
+
## Desenvolver / testar local (sem publicar)
|
|
24
|
+
```bash
|
|
25
|
+
cd tools/arzstack-installer
|
|
26
|
+
node src/index.mjs login --hub http://localhost:3000 --token arz_...
|
|
27
|
+
node src/index.mjs install <id-do-item> --project
|
|
28
|
+
# ou deixe o comando `arzstack-hub` global apontando pro código local:
|
|
29
|
+
npm link # cria o bin; rode: arzstack-hub install <id> ...
|
|
30
|
+
npm unlink -g @arzstack/hub # quando terminar
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Publicar no npm
|
|
34
|
+
Pré-requisitos (uma vez): conta npm (`npm login`) e o **scope `@arzstack`**
|
|
35
|
+
(criar a org `arzstack` em npmjs.com — grátis pra pacotes públicos).
|
|
36
|
+
```bash
|
|
37
|
+
cd tools/arzstack-installer
|
|
38
|
+
npm publish --access public # --access public é obrigatório p/ scope
|
|
39
|
+
```
|
|
40
|
+
Pra novas versões: suba `version` no `package.json` e `npm publish` de novo.
|
|
41
|
+
Recomendado **mover este diretório pra um repo próprio** (`arzstack-hub-cli`)
|
|
42
|
+
e publicar de lá — versionamento independente do portal.
|
|
43
|
+
|
|
44
|
+
## Usar (depois de publicado)
|
|
45
|
+
```bash
|
|
46
|
+
npx @arzstack/hub install <id> --global # sem instalar (npx baixa na hora)
|
|
47
|
+
# ou global:
|
|
48
|
+
npm i -g @arzstack/hub && arzstack-hub install <id> --global
|
|
49
|
+
```
|
|
50
|
+
Antes, autentique uma vez: `arzstack-hub login` (cola hub + token do Perfil).
|
|
51
|
+
|
|
52
|
+
Contrato completo: `docs/specs/install-experience.md` e `docs/specs/cli-auth.md`.
|
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arzstack/hub",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Instalador genérico de itens do ArzStack Hub (skills, MCPs, etc.).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"arzstack-hub": "./src/index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"files": ["src"],
|
|
13
|
+
"license": "UNLICENSED",
|
|
14
|
+
"private": false
|
|
15
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @arzstack/hub — instalador genérico de itens do ArzStack Hub.
|
|
3
|
+
// arzstack-hub login [--hub <url>] [--token <pat>]
|
|
4
|
+
// arzstack-hub install <id> [--global|--project] [--dir <path>] [--yes]
|
|
5
|
+
// arzstack-hub install <token-efêmero> ... (atalho copiado da web)
|
|
6
|
+
// Ver docs/specs/install-experience.md e docs/specs/cli-auth.md no repo do Hub.
|
|
7
|
+
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
import { join, resolve } from 'node:path';
|
|
12
|
+
import { stdin, stdout } from 'node:process';
|
|
13
|
+
import { createInterface } from 'node:readline/promises';
|
|
14
|
+
|
|
15
|
+
const CONFIG_DIR = join(homedir(), '.config', 'arzstack');
|
|
16
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
17
|
+
|
|
18
|
+
const fail = (m) => {
|
|
19
|
+
console.error(`\x1b[31m✗ ${m}\x1b[0m`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
};
|
|
22
|
+
const info = (m) => console.log(m);
|
|
23
|
+
|
|
24
|
+
function loadConfig() {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
|
|
27
|
+
} catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function saveConfig(cfg) {
|
|
32
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
33
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), { mode: 0o600 });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseArgs(argv) {
|
|
37
|
+
const [cmd, ...rest] = argv;
|
|
38
|
+
const flags = { target: 'global', yes: false, dir: null, token: null, hub: null };
|
|
39
|
+
let arg = null;
|
|
40
|
+
for (let i = 0; i < rest.length; i++) {
|
|
41
|
+
const a = rest[i];
|
|
42
|
+
if (a === '--global') flags.target = 'global';
|
|
43
|
+
else if (a === '--project') flags.target = 'project';
|
|
44
|
+
else if (a === '--yes' || a === '-y') flags.yes = true;
|
|
45
|
+
else if (a === '--dir') flags.dir = rest[++i];
|
|
46
|
+
else if (a === '--token') flags.token = rest[++i];
|
|
47
|
+
else if (a === '--hub') flags.hub = rest[++i];
|
|
48
|
+
else if (!a.startsWith('-') && arg === null) arg = a;
|
|
49
|
+
}
|
|
50
|
+
return { cmd, arg, flags };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function decodeToken(token) {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(Buffer.from(token.split('.')[0], 'base64url').toString('utf8'));
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function prompt(q) {
|
|
62
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
63
|
+
const a = await rl.question(q);
|
|
64
|
+
rl.close();
|
|
65
|
+
return a;
|
|
66
|
+
}
|
|
67
|
+
async function confirm(q, skip) {
|
|
68
|
+
if (skip) return true;
|
|
69
|
+
const a = (await prompt(`${q} [s/N] `)).trim().toLowerCase();
|
|
70
|
+
return a === 's' || a === 'sim' || a === 'y' || a === 'yes';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function run(cmd, args) {
|
|
74
|
+
info(`\x1b[2m$ ${cmd} ${args.join(' ')}\x1b[0m`);
|
|
75
|
+
const r = spawnSync(cmd, args, { stdio: 'inherit', shell: false });
|
|
76
|
+
if (r.status !== 0) fail(`Comando falhou (${cmd}).`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function skillsDir(target, override) {
|
|
80
|
+
if (override) return resolve(override);
|
|
81
|
+
return target === 'global'
|
|
82
|
+
? join(homedir(), '.agents', 'skills')
|
|
83
|
+
: join(process.cwd(), '.agents', 'skills');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function installSkill(m, flags) {
|
|
87
|
+
if (m.source?.kind !== 'git') fail('Esta skill não tem repositório git. Veja "Abrir recurso".');
|
|
88
|
+
const dest = join(skillsDir(flags.target, flags.dir), m.slug);
|
|
89
|
+
if (existsSync(dest)) fail(`Já existe: ${dest}.`);
|
|
90
|
+
info(`\nSkill: \x1b[1m${m.title}\x1b[0m\nRepo: ${m.source.repo}\nDest: ${dest}\n`);
|
|
91
|
+
if (!(await confirm('Clonar e instalar?', flags.yes))) fail('Cancelado.');
|
|
92
|
+
run('git', ['clone', '--depth', '1', m.source.repo, dest]);
|
|
93
|
+
info(`\n\x1b[32m✓ Skill instalada em ${dest}\x1b[0m`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function installMcp(m, flags) {
|
|
97
|
+
if (!m.run) fail('Sem comando de configuração do MCP. Veja "Abrir recurso".');
|
|
98
|
+
info(`\nMCP: \x1b[1m${m.title}\x1b[0m\nComando: ${m.run}\n`);
|
|
99
|
+
if (!(await confirm('Rodar a configuração?', flags.yes))) fail('Cancelado.');
|
|
100
|
+
const win = process.platform === 'win32';
|
|
101
|
+
run(win ? 'cmd' : 'sh', [win ? '/c' : '-c', m.run]);
|
|
102
|
+
info('\n\x1b[32m✓ MCP configurado.\x1b[0m');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function installOther(m) {
|
|
106
|
+
info(`\n\x1b[1m${m.title}\x1b[0m (${m.type})`);
|
|
107
|
+
if (m.source) info(`Repositório: ${m.source.repo}`);
|
|
108
|
+
if (m.run) info(`Passo: ${m.run}`);
|
|
109
|
+
info('Este tipo não tem instalação automática — siga o material acima.');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function report(hub, headers, body) {
|
|
113
|
+
try {
|
|
114
|
+
await fetch(`${hub}/api/install/report`, {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
117
|
+
body: JSON.stringify(body),
|
|
118
|
+
});
|
|
119
|
+
} catch {
|
|
120
|
+
/* best-effort */
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function cmdLogin(flags) {
|
|
125
|
+
const hub = (flags.hub || (await prompt('URL do Hub (ex.: https://hub.suaorg.com): '))).trim();
|
|
126
|
+
const token = (flags.token || (await prompt('Cole seu token (arz_…): '))).trim();
|
|
127
|
+
if (!token.startsWith('arz_')) fail('Token inválido (esperado arz_…).');
|
|
128
|
+
saveConfig({ hub: hub.replace(/\/+$/, ''), token });
|
|
129
|
+
info('\x1b[32m✓ Login salvo em ~/.config/arzstack/config.json\x1b[0m');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function cmdInstall(arg, flags) {
|
|
133
|
+
if (!arg) fail('Informe o id do item (ou o token copiado da web).');
|
|
134
|
+
|
|
135
|
+
let hub;
|
|
136
|
+
let manifestUrl;
|
|
137
|
+
let headers = {};
|
|
138
|
+
let reportBody = { target: flags.target };
|
|
139
|
+
|
|
140
|
+
if (arg.includes('.')) {
|
|
141
|
+
// token efêmero (auto-contido)
|
|
142
|
+
const p = decodeToken(arg);
|
|
143
|
+
if (!p?.hub || !p?.item) fail('Token inválido.');
|
|
144
|
+
hub = p.hub;
|
|
145
|
+
manifestUrl = `${hub}/api/install/manifest?token=${encodeURIComponent(arg)}`;
|
|
146
|
+
reportBody.token = arg;
|
|
147
|
+
} else {
|
|
148
|
+
// id do item + PAT
|
|
149
|
+
const cfg = loadConfig();
|
|
150
|
+
const token = flags.token || process.env.ARZSTACK_TOKEN || cfg.token;
|
|
151
|
+
hub = (flags.hub || process.env.ARZSTACK_HUB || cfg.hub || '').replace(/\/+$/, '');
|
|
152
|
+
if (!token || !hub) {
|
|
153
|
+
fail('Faça login: arzstack-hub login (ou passe --hub e --token).');
|
|
154
|
+
}
|
|
155
|
+
manifestUrl = `${hub}/api/install/manifest?item=${encodeURIComponent(arg)}`;
|
|
156
|
+
headers = { Authorization: `Bearer ${token}` };
|
|
157
|
+
reportBody.item = arg;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let manifest;
|
|
161
|
+
try {
|
|
162
|
+
const res = await fetch(manifestUrl, { headers });
|
|
163
|
+
if (!res.ok) fail(`Hub respondeu ${res.status}. (token expirado/revogado?)`);
|
|
164
|
+
manifest = await res.json();
|
|
165
|
+
} catch (e) {
|
|
166
|
+
fail(`Não consegui falar com o Hub: ${e.message}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (manifest.type === 'skill') await installSkill(manifest, flags);
|
|
170
|
+
else if (manifest.type === 'mcp_server') await installMcp(manifest, flags);
|
|
171
|
+
else installOther(manifest);
|
|
172
|
+
|
|
173
|
+
await report(hub, headers, reportBody);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function main() {
|
|
177
|
+
const { cmd, arg, flags } = parseArgs(process.argv.slice(2));
|
|
178
|
+
if (cmd === 'login') return cmdLogin(flags);
|
|
179
|
+
if (cmd === 'install') return cmdInstall(arg, flags);
|
|
180
|
+
info('Uso:');
|
|
181
|
+
info(' arzstack-hub login [--hub <url>] [--token <pat>]');
|
|
182
|
+
info(' arzstack-hub install <id|token> [--global|--project] [--dir <path>] [--yes]');
|
|
183
|
+
process.exit(cmd ? 1 : 0);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
main().catch((e) => fail(e?.message ?? String(e)));
|