@sandeco/antares 0.0.0-stage → 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sandeco
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,23 @@
1
- # Temporary Holding Version
1
+ # Antares para alunos
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Na pasta do seu canal, use o comando recebido no curso:
4
+
5
+ ```powershell
6
+ npx @sandeco/antares "<código>"
7
+ ```
8
+
9
+ Requer Node.js 18.20 ou superior e internet. Não é necessário login no npm nem no GitHub.
10
+ O npm pode perguntar se deseja obter o baixador na primeira execução; confirme.
11
+ O instalador pergunta se instala o Mira junto (`--com-mira` / `--sem-mira` pulam a pergunta).
12
+
13
+ ```powershell
14
+ npx @sandeco/antares "<código>" update # atualiza skills e runtime; nunca toca em _antares/
15
+ npx @sandeco/antares "<código>" doctor --online # dependências, Mira, conexão, MCPs
16
+ npx @sandeco/antares "<código>" --dir C:/meu-canal
17
+ ```
18
+
19
+ O código não diferencia maiúsculas, acentos, vírgulas ou espaços. Ele é conferido localmente para abrir um pacote cifrado; não é enviado pela rede nem salvo pelo baixador. Como qualquer argumento de terminal, pode aparecer no histórico ou na lista de processos do seu sistema. Os agentes do Antares não estão neste pacote npm.
20
+
21
+ O pacote aberto fica numa pasta temporária só durante o comando e é removido ao final. O que permanece no canal é o mesmo de sempre: `.claude/skills/antares*/`, `.agents/skills/antares*/`, `.antares/` e os dados em `_antares/`. Depois da instalação, abra a pasta no seu agente e chame o Antares (`/antares` no Claude Code, `$antares` no Codex, `antares` no Gemini CLI); o dia a dia não precisa de rede nem de código.
22
+
23
+ Um código compartilhado permite instalar em qualquer máquina que o possua. Não há identificação de aluno, expiração ou bloqueio de cópias já instaladas. Guarde o comando na área da turma.
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import { rodar } from '../src/bootstrap.mjs';
4
+
5
+ try {
6
+ const release = JSON.parse(fs.readFileSync(new URL('../release.json', import.meta.url), 'utf8'));
7
+ const { version } = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
8
+ process.exitCode = await rodar(process.argv.slice(2), release, { versao: version });
9
+ } catch (erro) {
10
+ console.error('Antares: ' + erro.message);
11
+ process.exitCode = 1;
12
+ }
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "@sandeco/antares",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Instalador do Antares para alunos com código de acesso",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "engines": { "node": ">=18.20.0" },
8
+ "bin": { "antares": "bin/antares.mjs" },
9
+ "files": ["bin/", "src/", "release.json", "README.md", "LICENSE"],
10
+ "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" }
11
+ }
package/release.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "schema": 1,
3
+ "runtimeVersion": "0.2.2",
4
+ "url": "https://gist.githubusercontent.com/sandeco/92fff05210d9c96d12cabe3282be5c9e/raw/28c590e44fd14a3a4550f8c7b7c3c0b02ed31381/antares.enc.json",
5
+ "sha256": "8f3d085254182671ad32065cc6de5148a820da6b089cfccac016e0b42fb7670d"
6
+ }
@@ -0,0 +1,110 @@
1
+ // Baixador público do Antares: confere o código, baixa o pacote cifrado fixado na release,
2
+ // abre em pasta temporária e chama o instalador privado (install, update ou doctor).
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { spawn } from 'node:child_process';
7
+ import { LIMITE, abrir, normalizarCodigo } from './pacote.mjs';
8
+
9
+ export const COMANDOS = ['install', 'update', 'doctor'];
10
+
11
+ const AJUDA = `Antares: instalação com código do curso
12
+ Uso: npx @sandeco/antares "<código>" [install|update|doctor] [opções]
13
+ install (padrão) instala o Antares na pasta atual (--com-mira | --sem-mira)
14
+ update atualiza skills e runtime; nunca toca em _antares/
15
+ doctor confere dependências, Mira, conexão e MCPs (--online)
16
+ Opções: --dir <pasta> para outra pasta.
17
+ Node.js 18.20 ou superior. Não é necessário login no npm nem no GitHub.
18
+ O código não é enviado pela rede nem salvo pelo baixador.
19
+ `;
20
+
21
+ export function interpretar(args, cwd = process.cwd()) {
22
+ if (!args.length || ['--help', '-h'].includes(args[0])) return { ajuda: true };
23
+ if (args[0] === '--version') return { versao: true };
24
+ const posicionais = [];
25
+ let i = 0;
26
+ for (; i < args.length && !args[i].startsWith('-'); i++) posicionais.push(args[i]);
27
+ if (posicionais.some(p => p.length > 1024 || /[\x00-\x1f\x7f]/.test(p))) throw new Error('Informe o código recebido no curso.');
28
+ const comando = posicionais.length > 1 && COMANDOS.includes(posicionais[posicionais.length - 1]) ? posicionais.pop() : 'install';
29
+ const codigo = normalizarCodigo(posicionais);
30
+ const opcoes = [];
31
+ let pasta = cwd;
32
+ let informada = false;
33
+ for (; i < args.length; i++) {
34
+ if (args[i] === '--dir' || args[i].startsWith('--dir=')) {
35
+ if (informada) throw new Error('Opção duplicada: dir');
36
+ informada = true;
37
+ const valor = args[i] === '--dir' ? args[++i] : args[i].slice('--dir='.length);
38
+ if (!valor || valor.startsWith('--')) throw new Error('Valor ausente: dir');
39
+ pasta = path.resolve(cwd, valor);
40
+ } else opcoes.push(args[i]);
41
+ }
42
+ return { codigo, comando, opcoes, pasta: path.resolve(pasta) };
43
+ }
44
+
45
+ export function validarRelease(release) {
46
+ if (release?.schema !== 1 || !/^\d+\.\d+\.\d+$/.test(release.runtimeVersion) || !/^[a-f0-9]{64}$/.test(release.sha256)) throw new Error('Release não configurada pelo mantenedor.');
47
+ let url;
48
+ try { url = new URL(release.url); } catch { throw new Error('Release não configurada pelo mantenedor.'); }
49
+ if (url.protocol !== 'https:' || url.hostname !== 'gist.githubusercontent.com' || url.port || url.username || url.password || url.search || url.hash || !/^\/[A-Za-z0-9-]+\/[a-f0-9]+\/raw\/[a-f0-9]{40}\/[A-Za-z0-9_.-]+$/.test(url.pathname)) throw new Error('Endereço da release inválido.');
50
+ return url.href;
51
+ }
52
+
53
+ export async function baixar(release, fetcher = fetch) {
54
+ const url = validarRelease(release);
55
+ const signal = AbortSignal.timeout(30_000);
56
+ const resposta = await fetcher(url, { redirect: 'error', signal, headers: { Accept: 'application/json' } });
57
+ if (!resposta.ok) throw new Error(`Download indisponível (HTTP ${resposta.status}). Tente novamente mais tarde.`);
58
+ const declarado = resposta.headers.get('content-length');
59
+ if (declarado !== null && (!/^\d+$/.test(declarado) || Number(declarado) > LIMITE)) throw new Error('Download excede o limite permitido.');
60
+ if (!resposta.body) throw new Error('Download vazio.');
61
+ const partes = [];
62
+ let tamanho = 0;
63
+ for await (const parte of resposta.body) {
64
+ tamanho += parte.length;
65
+ if (tamanho > LIMITE) throw new Error('Download excede o limite permitido.');
66
+ partes.push(parte);
67
+ }
68
+ return Buffer.concat(partes);
69
+ }
70
+
71
+ // Grava o pacote numa pasta temporária nova; quem chama remove ao terminar.
72
+ export function materializar(pacote) {
73
+ const destino = fs.mkdtempSync(path.join(os.tmpdir(), 'antares-pacote-'));
74
+ for (const arquivo of pacote.files) {
75
+ const alvo = path.join(destino, arquivo.path);
76
+ fs.mkdirSync(path.dirname(alvo), { recursive: true });
77
+ fs.writeFileSync(alvo, arquivo.content, { flag: 'wx', mode: arquivo.mode });
78
+ }
79
+ return destino;
80
+ }
81
+
82
+ export function executar(runtime, comando, pasta, opcoes) {
83
+ return new Promise((resolve, reject) => {
84
+ const filho = spawn(process.execPath, [path.join(runtime, 'bin/antares.js'), comando, ...opcoes, '--dir', pasta], { cwd: process.cwd(), stdio: 'inherit', shell: false, windowsHide: true });
85
+ const int = () => filho.kill('SIGINT');
86
+ const term = () => filho.kill('SIGTERM');
87
+ process.on('SIGINT', int);
88
+ process.on('SIGTERM', term);
89
+ const limpar = () => { process.off('SIGINT', int); process.off('SIGTERM', term); };
90
+ filho.once('error', erro => { limpar(); reject(erro); });
91
+ filho.once('close', (codigo, sinal) => { limpar(); resolve(codigo ?? (sinal === 'SIGINT' ? 130 : 1)); });
92
+ });
93
+ }
94
+
95
+ export async function rodar(args, release, deps = {}) {
96
+ const p = interpretar(args, deps.cwd ?? process.cwd());
97
+ const log = deps.log ?? console.log;
98
+ if (p.ajuda) { log(AJUDA); return 0; }
99
+ if (p.versao) { log(deps.versao ?? '0.0.0'); return 0; }
100
+ if (Number(process.versions.node.split('.')[0]) < 18) throw new Error('Instale Node.js 18.20 ou superior.');
101
+ const bytes = await baixar(release, deps.fetcher);
102
+ const pacote = await abrir(bytes, p.codigo, release);
103
+ const runtime = materializar(pacote);
104
+ try {
105
+ log(`Antares: código aceito. Abrindo ${p.comando}…`);
106
+ return await (deps.executar ?? executar)(runtime, p.comando, p.pasta, p.opcoes);
107
+ } finally {
108
+ fs.rmSync(runtime, { recursive: true, force: true });
109
+ }
110
+ }
package/src/pacote.mjs ADDED
@@ -0,0 +1,93 @@
1
+ // Abertura e validação do pacote cifrado do Antares (AES-256-GCM, chave derivada por scrypt).
2
+ import { createHash, createDecipheriv, scrypt as scryptCallback } from 'node:crypto';
3
+ import { promisify } from 'node:util';
4
+ import { gunzipSync } from 'node:zlib';
5
+
6
+ export const LIMITE = 4 * 1024 * 1024;
7
+ export const LIMITE_ABERTO = 8 * 1024 * 1024;
8
+ export const KDF = Object.freeze({ N: 65536, r: 8, p: 1, maxmem: 128 * 1024 * 1024 });
9
+ export const AAD = Buffer.from('antares:pacote:v1');
10
+ export const NOME_PACOTE = 'antares-agents';
11
+ export const hash = valor => createHash('sha256').update(valor).digest('hex');
12
+ export const derivar = (chave, salt) => promisify(scryptCallback)(chave, salt, 32, KDF);
13
+ const invalido = () => { throw new Error('Pacote de distribuição inválido.'); };
14
+
15
+ // O código ignora maiúsculas, acentos, vírgulas e espaços: "Exemplo, Código, Exemplo"
16
+ // vale o mesmo com ou sem aspas, em qualquer shell.
17
+ export function normalizarCodigo(partes) {
18
+ const texto = Array.isArray(partes) ? partes.join(' ') : String(partes ?? '');
19
+ const codigo = texto.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().replace(/[^a-z0-9]/g, '');
20
+ if (codigo.length < 12 || codigo.length > 1024) throw new Error('Informe o código recebido no curso.');
21
+ return codigo;
22
+ }
23
+
24
+ export function decode64(valor, tamanho) {
25
+ if (typeof valor !== 'string' || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(valor)) invalido();
26
+ const bytes = Buffer.from(valor, 'base64');
27
+ if (tamanho !== undefined && bytes.length !== tamanho) invalido();
28
+ return bytes;
29
+ }
30
+
31
+ export function entradaSegura(relativo) {
32
+ if (typeof relativo !== 'string' || relativo.length > 240 || !relativo.length || relativo.includes('\\')) invalido();
33
+ const partes = relativo.split('/');
34
+ if (partes.some(p => !/^[a-zA-Z0-9_.-]+$/.test(p) || p === '.' || p === '..' || /[. ]$/.test(p) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(p))) invalido();
35
+ const raizes = ['LICENSE', 'README.md', 'package.json'];
36
+ if (!raizes.includes(relativo) && !/^(agents|bin|lib|templates|docs)\//.test(relativo) && !/^node_modules\/[a-z0-9_.-]+\/.+/.test(relativo)) invalido();
37
+ return relativo;
38
+ }
39
+
40
+ export function validarPacote(pacote, versao) {
41
+ if (pacote?.schema !== 1 || pacote.name !== NOME_PACOTE || pacote.version !== versao || !Array.isArray(pacote.files) || !pacote.files.length || pacote.files.length > 1000) invalido();
42
+ const vistos = new Set();
43
+ let total = 0;
44
+ const arquivos = pacote.files.map(arquivo => {
45
+ const relativo = entradaSegura(arquivo?.path), minusculo = relativo.toLowerCase();
46
+ if (vistos.has(minusculo)) invalido();
47
+ vistos.add(minusculo);
48
+ const conteudo = decode64(arquivo.content);
49
+ total += conteudo.length;
50
+ if (total > LIMITE_ABERTO || hash(conteudo) !== arquivo.sha256) invalido();
51
+ if (![0o644, 0o755].includes(arquivo.mode)) invalido();
52
+ return { path: relativo, content: conteudo, mode: arquivo.mode, sha256: arquivo.sha256 };
53
+ });
54
+ // Um arquivo não pode ser também um diretório ancestral de outro.
55
+ for (const nome of vistos) {
56
+ const partes = nome.split('/');
57
+ while (partes.length > 1) { partes.pop(); if (vistos.has(partes.join('/'))) invalido(); }
58
+ }
59
+ const manifesto = arquivos.find(a => a.path === 'package.json');
60
+ if (!manifesto || !arquivos.some(a => a.path === 'bin/antares.js')) invalido();
61
+ const pkg = JSON.parse(manifesto.content.toString('utf8'));
62
+ if (pkg.name !== pacote.name || pkg.version !== versao) invalido();
63
+ // Dependências de runtime vêm vendorizadas no pacote; nada além delas em node_modules/.
64
+ const dependencias = Object.keys(pkg.dependencies ?? {});
65
+ if (dependencias.some(dep => !vistos.has(`node_modules/${dep}/package.json`))) invalido();
66
+ for (const nome of vistos) {
67
+ const m = /^node_modules\/([^/]+)\//.exec(nome);
68
+ if (m && !dependencias.includes(m[1])) invalido();
69
+ }
70
+ return { ...pacote, files: arquivos };
71
+ }
72
+
73
+ export async function abrir(bytes, codigo, release) {
74
+ if (!Buffer.isBuffer(bytes) || bytes.length > LIMITE || hash(bytes) !== release.sha256) throw new Error('Integridade do download divergente; instalação cancelada.');
75
+ const envelope = JSON.parse(bytes.toString('utf8'));
76
+ if (envelope?.schema !== 1 || envelope.kdf !== 'scrypt-65536-8-1' || envelope.cipher !== 'aes-256-gcm') invalido();
77
+ const salt = decode64(envelope.salt, 32), iv = decode64(envelope.iv, 12), tag = decode64(envelope.tag, 16);
78
+ const cifrado = decode64(envelope.data);
79
+ let derivada;
80
+ let comprimido;
81
+ try {
82
+ derivada = await derivar(codigo, salt);
83
+ const decipher = createDecipheriv('aes-256-gcm', derivada, iv);
84
+ decipher.setAAD(AAD);
85
+ decipher.setAuthTag(tag);
86
+ comprimido = Buffer.concat([decipher.update(cifrado), decipher.final()]);
87
+ } catch {
88
+ throw new Error('Código de acesso inválido. Confira o comando recebido no curso.');
89
+ } finally { derivada?.fill(0); }
90
+ const aberto = gunzipSync(comprimido, { maxOutputLength: LIMITE_ABERTO });
91
+ try { return validarPacote(JSON.parse(aberto.toString('utf8')), release.runtimeVersion); }
92
+ finally { aberto.fill(0); comprimido.fill(0); }
93
+ }