@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.
Files changed (3) hide show
  1. package/README.md +52 -0
  2. package/package.json +15 -0
  3. 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)));