@neetru/cli 2.9.13 → 2.10.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/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  Convenção: [Keep a Changelog](https://keepachangelog.com/) + [SemVer](https://semver.org/).
4
4
 
5
+ ## [2.10.0] — 2026-05-27 (rename: `neetru marketplace` → `neetru archive` · fonte agora pública)
6
+
7
+ ### BREAKING
8
+
9
+ - **`neetru marketplace` foi renomeado para `neetru archive`.** Subcomandos preservados (`skills install/update/list/uninstall`, `sdk init/templates/add`, `browse`). NÃO há alias de retrocompat — o comando antigo deixa de existir nesta versão. Quem rodava `neetru marketplace skills install` em scripts CI precisa trocar para `neetru archive skills install`.
10
+ - **Cache local mudou de `~/.neetru/marketplace-cache/` para `~/.neetru/archive-cache/`.** Caches antigos podem ser apagados manualmente (não há migração automática — `archive skills install` cria o novo limpo). A função `getSkillsSourceDir` mantém retrocompat detectando o nível `claude-skills/` caso o cache de uma versão anterior do `archive-cache` ainda tenha esse layout.
11
+ - **Variável de ambiente `NEETRU_MARKETPLACE_SYMLINK` virou `NEETRU_ARCHIVE_SYMLINK`** (Windows). Quem usava precisa renomear.
12
+
13
+ ### Mudado
14
+
15
+ - **Fonte de skills migrou de `github.com/Neetru/neetru-libs` (privado) para `github.com/Neetru/neetru-libs-public` (público).** O `neetru-libs` privado mantém `packages/` (`@neetru/db-classifier`, `sql-guard`, `pii-mask`, `neetru-glossario`) — só `claude-skills/` foi exposto, via `git subtree push --prefix=claude-skills`. Resultado: `neetru archive skills install` deixa de exigir `gh auth setup-git` ou `GITHUB_TOKEN` — clone anônimo funciona.
16
+ - **Layout do clone público é mais flat:** `plugins/<plugin>/skills/<skill>/SKILL.md` no topo (em vez de `claude-skills/plugins/...`). O resolver detecta o layout legado automaticamente — caches antigos continuam resolvendo até o próximo `archive skills update`.
17
+
18
+ ### Por quê
19
+
20
+ - O 2.9.13 quebrava em `git clone` quando `neetru-libs` virou privado (HTTP 401 sem credencial git configurada). O fix arquitetural certo era separar o catálogo público de skills do código proprietário em `packages/` — `neetru-libs-public` é esse catálogo. A renomeação `marketplace → archive` aproveitou o break pra alinhar o nome do comando ao conceito real (catálogo/arquivo de artefatos, não um marketplace transacional).
21
+
5
22
  ## [2.9.13] — 2026-05-26 (feat: force-cleanup recovery + mfa-status observability)
6
23
 
7
24
  ### Adicionado
@@ -0,0 +1,37 @@
1
+ export declare function getCacheDir(): string;
2
+ /**
3
+ * Resolve o diretorio fonte das skills no cache local.
4
+ *
5
+ * Layout esperado no repo `neetru-libs-public` (top-level):
6
+ *
7
+ * 1. Estruturado (atual): plugins/PLUGIN/skills/SKILL/SKILL.md
8
+ * - descobre todos os plugins/PLUGIN/skills/ e retorna o primeiro.
9
+ * 2. Plugin direto (legado): skills/SKILL/SKILL.md
10
+ * - fallback se nao houver plugins/.
11
+ *
12
+ * Retrocompat: caches antigos (`marketplace-cache` ou versoes anteriores do
13
+ * archive-cache) podem ter o nivel `claude-skills/` no topo. Se detectarmos
14
+ * esse layout, adicionamos o prefixo na resolucao — assim um cache stale
15
+ * nao quebra o comando antes do proximo `archive skills update`.
16
+ */
17
+ export declare function getSkillsSourceDir(): string;
18
+ export declare function getClaudeSkillsDir(): string;
19
+ export interface SkillInfo {
20
+ name: string;
21
+ description: string;
22
+ version: string;
23
+ installed: boolean;
24
+ }
25
+ export declare function listAvailableSkills(): Promise<SkillInfo[]>;
26
+ export declare function runSkillsInstall(): Promise<void>;
27
+ export declare function runSkillsUpdate(): Promise<void>;
28
+ export declare function runSkillsList(): Promise<void>;
29
+ export declare function runSkillsUninstall(opts?: {
30
+ yes?: boolean;
31
+ }): Promise<void>;
32
+ export declare function runSdkInit(): Promise<void>;
33
+ export declare function runSdkTemplates(): Promise<void>;
34
+ export declare function runSdkAddTemplate(template: string, opts?: {
35
+ force?: boolean;
36
+ }): Promise<void>;
37
+ export declare function runArchiveBrowse(): Promise<void>;
@@ -0,0 +1,591 @@
1
+ /**
2
+ * `neetru archive` — central de artefatos do ecossistema Neetru.
3
+ *
4
+ * Dois grupos principais:
5
+ * skills — instalar/atualizar/listar/remover Skills do Claude Code
6
+ * sdk — adicionar @neetru/sdk ao projeto atual ou bootstrapar novo
7
+ * browse — abre https://github.com/Neetru/neetru-libs-public no browser
8
+ *
9
+ * Fonte canônica de skills: https://github.com/Neetru/neetru-libs-public
10
+ * (espelho público da pasta `claude-skills/` do `neetru-libs` privado,
11
+ * sincronizado via `git subtree push --prefix=claude-skills`).
12
+ * Cache local: ~/.neetru/archive-cache/
13
+ * Destino das skills: ~/.claude/skills/<nome-da-skill>/
14
+ *
15
+ * Decisão de design (Windows): symlinks requerem privilégio de admin
16
+ * ou Developer Mode habilitado. O comando usa cópia recursiva por
17
+ * default no Windows — sem surpresa pra quem não tem Developer Mode.
18
+ * Se NEETRU_ARCHIVE_SYMLINK=1, tenta symlink mesmo no Windows.
19
+ */
20
+ import * as fs from 'node:fs';
21
+ import * as fsp from 'node:fs/promises';
22
+ import * as path from 'node:path';
23
+ import * as os from 'node:os';
24
+ import { spawnSync } from 'node:child_process';
25
+ import chalk from 'chalk';
26
+ import ora from 'ora';
27
+ import inquirer from 'inquirer';
28
+ import { log } from '../utils/logger.js';
29
+ import { safeExit } from '../utils/safe-exit.js';
30
+ // ─── constantes ────────────────────────────────────────────────────────────────
31
+ const LIBS_REPO_URL = 'https://github.com/Neetru/neetru-libs-public.git';
32
+ const LIBS_REPO_HTTPS = 'https://github.com/Neetru/neetru-libs-public';
33
+ const SDK_PACKAGE = '@neetru/sdk';
34
+ const SDK_VERSION = '^2.1.0';
35
+ export function getCacheDir() {
36
+ return path.join(os.homedir(), '.neetru', 'archive-cache');
37
+ }
38
+ /**
39
+ * Resolve o diretorio fonte das skills no cache local.
40
+ *
41
+ * Layout esperado no repo `neetru-libs-public` (top-level):
42
+ *
43
+ * 1. Estruturado (atual): plugins/PLUGIN/skills/SKILL/SKILL.md
44
+ * - descobre todos os plugins/PLUGIN/skills/ e retorna o primeiro.
45
+ * 2. Plugin direto (legado): skills/SKILL/SKILL.md
46
+ * - fallback se nao houver plugins/.
47
+ *
48
+ * Retrocompat: caches antigos (`marketplace-cache` ou versoes anteriores do
49
+ * archive-cache) podem ter o nivel `claude-skills/` no topo. Se detectarmos
50
+ * esse layout, adicionamos o prefixo na resolucao — assim um cache stale
51
+ * nao quebra o comando antes do proximo `archive skills update`.
52
+ */
53
+ export function getSkillsSourceDir() {
54
+ const cacheRoot = getCacheDir();
55
+ const legacyRoot = path.join(cacheRoot, 'claude-skills');
56
+ const rootCandidate = fs.existsSync(path.join(legacyRoot, 'plugins'))
57
+ ? legacyRoot
58
+ : cacheRoot;
59
+ const pluginsRoot = path.join(rootCandidate, 'plugins');
60
+ try {
61
+ const pluginDirs = fs.readdirSync(pluginsRoot, { withFileTypes: true })
62
+ .filter((e) => e.isDirectory())
63
+ .map((e) => path.join(pluginsRoot, e.name, 'skills'))
64
+ .filter((p) => fs.existsSync(p));
65
+ if (pluginDirs.length > 0) {
66
+ // Retorna o primeiro plugin's skills dir (no estado atual: só 1 plugin "neetru")
67
+ // Futuramente: expandir pra agregar de múltiplos plugins se houver demanda.
68
+ return pluginDirs[0];
69
+ }
70
+ }
71
+ catch {
72
+ // pluginsRoot não existe — cai pro fallback
73
+ }
74
+ return path.join(rootCandidate, 'skills');
75
+ }
76
+ export function getClaudeSkillsDir() {
77
+ return path.join(os.homedir(), '.claude', 'skills');
78
+ }
79
+ function isWindows() {
80
+ return process.platform === 'win32';
81
+ }
82
+ // ─── helpers de filesystem ─────────────────────────────────────────────────────
83
+ async function ensureDir(dir) {
84
+ await fsp.mkdir(dir, { recursive: true });
85
+ }
86
+ async function dirExists(p) {
87
+ try {
88
+ const stat = await fsp.stat(p);
89
+ return stat.isDirectory();
90
+ }
91
+ catch {
92
+ return false;
93
+ }
94
+ }
95
+ async function pathExists(p) {
96
+ try {
97
+ await fsp.access(p);
98
+ return true;
99
+ }
100
+ catch {
101
+ return false;
102
+ }
103
+ }
104
+ /** Cópia recursiva pura em Node (sem dependência extra). */
105
+ async function copyDirRecursive(src, dest) {
106
+ await ensureDir(dest);
107
+ const entries = await fsp.readdir(src, { withFileTypes: true });
108
+ for (const entry of entries) {
109
+ const srcPath = path.join(src, entry.name);
110
+ const destPath = path.join(dest, entry.name);
111
+ if (entry.isDirectory()) {
112
+ await copyDirRecursive(srcPath, destPath);
113
+ }
114
+ else {
115
+ await fsp.copyFile(srcPath, destPath);
116
+ }
117
+ }
118
+ }
119
+ async function removeDir(p) {
120
+ await fsp.rm(p, { recursive: true, force: true });
121
+ }
122
+ // ─── git helpers (spawnSync — sem shell injection) ─────────────────────────────
123
+ function gitAvailable() {
124
+ const r = spawnSync('git', ['--version'], { stdio: 'ignore' });
125
+ return r.status === 0;
126
+ }
127
+ function gitClone(dest) {
128
+ const r = spawnSync('git', ['clone', '--depth', '1', LIBS_REPO_URL, dest], {
129
+ encoding: 'utf8',
130
+ });
131
+ return { ok: r.status === 0, stderr: r.stderr ?? '' };
132
+ }
133
+ function gitPull(dir) {
134
+ const r = spawnSync('git', ['pull', '--ff-only'], {
135
+ cwd: dir,
136
+ encoding: 'utf8',
137
+ });
138
+ return { ok: r.status === 0, stderr: r.stderr ?? '' };
139
+ }
140
+ // ─── symlink vs cópia ──────────────────────────────────────────────────────────
141
+ /**
142
+ * true se deve tentar symlink.
143
+ * Linux/macOS: sim por default.
144
+ * Windows: só com NEETRU_MARKETPLACE_SYMLINK=1 (requer Developer Mode ou admin).
145
+ */
146
+ function useSymlink() {
147
+ if (process.env.NEETRU_MARKETPLACE_SYMLINK === '1')
148
+ return true;
149
+ return !isWindows();
150
+ }
151
+ async function linkOrCopy(src, dest) {
152
+ if (useSymlink()) {
153
+ try {
154
+ await fsp.symlink(src, dest, 'dir');
155
+ return 'symlink';
156
+ }
157
+ catch {
158
+ // fallback pra cópia (sem acesso pra symlink)
159
+ }
160
+ }
161
+ await copyDirRecursive(src, dest);
162
+ return 'copy';
163
+ }
164
+ async function parseSkillMd(skillMdPath) {
165
+ try {
166
+ const raw = await fsp.readFile(skillMdPath, 'utf8');
167
+ // Primeira linha de parágrafo após o heading
168
+ const descMatch = raw.match(/^#{1,3}\s+.+\n+([^#\n].+)/m);
169
+ const versionMatch = raw.match(/version[:\s]+([0-9]+\.[0-9]+\.[0-9]+[\w.-]*)/i);
170
+ return {
171
+ description: descMatch?.[1]?.slice(0, 80).trim() ?? '(sem descrição)',
172
+ version: versionMatch?.[1] ?? '—',
173
+ };
174
+ }
175
+ catch {
176
+ return { description: '(SKILL.md ilegível)', version: '—' };
177
+ }
178
+ }
179
+ export async function listAvailableSkills() {
180
+ const sourceDir = getSkillsSourceDir();
181
+ const claudeDir = getClaudeSkillsDir();
182
+ if (!(await dirExists(sourceDir)))
183
+ return [];
184
+ const entries = await fsp.readdir(sourceDir, { withFileTypes: true });
185
+ const skills = [];
186
+ for (const entry of entries) {
187
+ if (!entry.isDirectory())
188
+ continue;
189
+ const skillMdPath = path.join(sourceDir, entry.name, 'SKILL.md');
190
+ const { description, version } = await parseSkillMd(skillMdPath);
191
+ const installed = await pathExists(path.join(claudeDir, entry.name));
192
+ skills.push({ name: entry.name, description, version, installed });
193
+ }
194
+ return skills;
195
+ }
196
+ // ─── neetru archiveskills install ─────────────────────────────────────────
197
+ export async function runSkillsInstall() {
198
+ if (!gitAvailable()) {
199
+ log.error('git não encontrado. Instale o git e tente novamente.');
200
+ log.dim(' → https://git-scm.com/downloads');
201
+ safeExit(1);
202
+ }
203
+ const cacheDir = getCacheDir();
204
+ const claudeDir = getClaudeSkillsDir();
205
+ const spinner = ora({ text: 'Verificando cache local…', color: 'cyan' }).start();
206
+ await ensureDir(cacheDir);
207
+ await ensureDir(claudeDir);
208
+ const isFirstRun = !(await dirExists(path.join(cacheDir, '.git')));
209
+ if (isFirstRun) {
210
+ spinner.text = `Clonando ${LIBS_REPO_URL}…`;
211
+ const { ok, stderr } = gitClone(cacheDir);
212
+ if (!ok) {
213
+ spinner.fail('Falha ao clonar repositório.');
214
+ log.error(stderr.trim() || 'Verifique sua conexão e acesso ao GitHub.');
215
+ safeExit(1);
216
+ }
217
+ }
218
+ else {
219
+ spinner.text = 'Sincronizando cache (git pull)…';
220
+ const { ok, stderr } = gitPull(cacheDir);
221
+ if (!ok) {
222
+ spinner.warn(`Aviso no git pull: ${stderr.trim().split('\n')[0]}`);
223
+ }
224
+ }
225
+ spinner.succeed('Cache sincronizado.');
226
+ const skills = await listAvailableSkills();
227
+ if (skills.length === 0) {
228
+ log.warn('Nenhuma skill encontrada no cache.');
229
+ log.dim(' Estrutura esperada: plugins/<plugin>/skills/<skill>/SKILL.md');
230
+ log.dim(' (ou legado: skills/<skill>/SKILL.md)');
231
+ return;
232
+ }
233
+ console.log();
234
+ log.heading('Instalando skills');
235
+ let installed = 0;
236
+ let updated = 0;
237
+ let skipped = 0;
238
+ for (const skill of skills) {
239
+ const srcPath = path.join(getSkillsSourceDir(), skill.name);
240
+ const destPath = path.join(claudeDir, skill.name);
241
+ if (skill.installed) {
242
+ const isLink = fs.existsSync(destPath) && fs.lstatSync(destPath).isSymbolicLink();
243
+ if (isLink) {
244
+ console.log(` ${chalk.dim('─')} ${chalk.bold(skill.name)} — symlink ativo`);
245
+ skipped++;
246
+ }
247
+ else {
248
+ // Cópia existente → re-copia pra atualizar
249
+ await removeDir(destPath);
250
+ await copyDirRecursive(srcPath, destPath);
251
+ console.log(` ${chalk.yellow('↑')} ${chalk.bold(skill.name)} — atualizado`);
252
+ updated++;
253
+ }
254
+ }
255
+ else {
256
+ const mode = await linkOrCopy(srcPath, destPath);
257
+ const modeLabel = mode === 'symlink' ? chalk.dim('symlink') : chalk.dim('cópia');
258
+ console.log(` ${chalk.green('✓')} ${chalk.bold(skill.name)} — instalado (${modeLabel})`);
259
+ console.log(` ${chalk.dim('→')} ${chalk.dim(destPath)}`);
260
+ installed++;
261
+ }
262
+ }
263
+ console.log();
264
+ const parts = [];
265
+ if (installed > 0)
266
+ parts.push(`${installed} nova(s)`);
267
+ if (updated > 0)
268
+ parts.push(`${updated} atualizada(s)`);
269
+ if (skipped > 0)
270
+ parts.push(`${skipped} sem alteração`);
271
+ log.success(parts.join(', ') + '.');
272
+ log.dim(' Reinicie o Claude Code para carregar as skills.');
273
+ }
274
+ // ─── neetru archiveskills update ──────────────────────────────────────────
275
+ export async function runSkillsUpdate() {
276
+ const cacheDir = getCacheDir();
277
+ const hasCache = await dirExists(path.join(cacheDir, '.git'));
278
+ if (!hasCache) {
279
+ log.warn('Cache não encontrado. Rodando install completo…');
280
+ await runSkillsInstall();
281
+ return;
282
+ }
283
+ const spinner = ora({ text: 'Atualizando neetru-libs (git pull)…', color: 'cyan' }).start();
284
+ const { ok, stderr } = gitPull(cacheDir);
285
+ if (!ok) {
286
+ spinner.fail('Falha ao atualizar cache.');
287
+ log.error(stderr.trim() || 'Verifique sua conexão.');
288
+ safeExit(1);
289
+ }
290
+ spinner.succeed('Repositório atualizado.');
291
+ // Re-copia skills instaladas como cópia (symlinks já apontam pro cache atualizado)
292
+ const claudeDir = getClaudeSkillsDir();
293
+ const skills = await listAvailableSkills();
294
+ let recoped = 0;
295
+ for (const skill of skills) {
296
+ if (!skill.installed)
297
+ continue;
298
+ const destPath = path.join(claudeDir, skill.name);
299
+ const isLink = fs.existsSync(destPath) && fs.lstatSync(destPath).isSymbolicLink();
300
+ if (isLink)
301
+ continue; // symlink aponta pro cache — já atualizado pelo git pull
302
+ const srcPath = path.join(getSkillsSourceDir(), skill.name);
303
+ await removeDir(destPath);
304
+ await copyDirRecursive(srcPath, destPath);
305
+ console.log(` ${chalk.yellow('↑')} ${chalk.bold(skill.name)} — re-copiado`);
306
+ recoped++;
307
+ }
308
+ if (recoped === 0) {
309
+ log.success('Skills atualizadas via cache (symlinks).');
310
+ }
311
+ else {
312
+ log.success(`${recoped} skill(s) re-copiada(s).`);
313
+ }
314
+ log.dim(' Reinicie o Claude Code se novas skills foram adicionadas.');
315
+ }
316
+ // ─── neetru archiveskills list ────────────────────────────────────────────
317
+ export async function runSkillsList() {
318
+ const hasCache = await dirExists(getSkillsSourceDir());
319
+ if (!hasCache) {
320
+ log.warn('Sem cache local. Rode: neetru archive skills install');
321
+ return;
322
+ }
323
+ const skills = await listAvailableSkills();
324
+ if (skills.length === 0) {
325
+ log.warn('Nenhuma skill encontrada no cache.');
326
+ return;
327
+ }
328
+ console.log();
329
+ log.heading('Skills do ecossistema Neetru');
330
+ console.log();
331
+ const col1 = Math.max(6, ...skills.map((s) => s.name.length)) + 2;
332
+ const col2 = Math.max(7, ...skills.map((s) => s.version.length)) + 2;
333
+ console.log(` ${chalk.dim('NOME'.padEnd(col1))}${chalk.dim('VERSÃO'.padEnd(col2))}${chalk.dim('STATUS'.padEnd(13))}${chalk.dim('DESCRIÇÃO')}`);
334
+ console.log(` ${chalk.dim('─'.repeat(col1 + col2 + 40))}`);
335
+ for (const skill of skills) {
336
+ const statusLabel = skill.installed ? chalk.green('instalada') : chalk.dim('disponível');
337
+ const name = chalk.bold(skill.name.padEnd(col1));
338
+ const version = chalk.dim(skill.version.padEnd(col2));
339
+ const desc = chalk.dim(skill.description);
340
+ console.log(` ${name}${version}${statusLabel.padEnd(13)} ${desc}`);
341
+ }
342
+ console.log();
343
+ }
344
+ // ─── neetru archiveskills uninstall ───────────────────────────────────────
345
+ export async function runSkillsUninstall(opts = {}) {
346
+ const claudeDir = getClaudeSkillsDir();
347
+ const skills = await listAvailableSkills();
348
+ const installed = skills.filter((s) => s.installed);
349
+ if (installed.length === 0) {
350
+ log.info('Nenhuma skill Neetru instalada.');
351
+ return;
352
+ }
353
+ console.log();
354
+ log.heading('Skills a remover');
355
+ for (const s of installed) {
356
+ console.log(` ${chalk.red('·')} ${chalk.bold(s.name)}`);
357
+ console.log(` ${chalk.dim(path.join(claudeDir, s.name))}`);
358
+ }
359
+ console.log();
360
+ if (!opts.yes) {
361
+ const { confirm } = await inquirer.prompt([
362
+ {
363
+ type: 'confirm',
364
+ name: 'confirm',
365
+ message: `Remover ${installed.length} skill(s) de ${chalk.dim('~/.claude/skills/')}?`,
366
+ default: false,
367
+ },
368
+ ]);
369
+ if (!confirm) {
370
+ log.dim('Operação cancelada.');
371
+ return;
372
+ }
373
+ }
374
+ for (const skill of installed) {
375
+ const destPath = path.join(claudeDir, skill.name);
376
+ await removeDir(destPath);
377
+ console.log(` ${chalk.red('✗')} ${chalk.bold(skill.name)} removido`);
378
+ }
379
+ console.log();
380
+ log.success('Skills removidas. Reinicie o Claude Code.');
381
+ }
382
+ // ─── neetru archivesdk init ───────────────────────────────────────────────
383
+ export async function runSdkInit() {
384
+ const cwd = process.cwd();
385
+ const pkgJsonPath = path.join(cwd, 'package.json');
386
+ const hasPkgJson = await pathExists(pkgJsonPath);
387
+ if (!hasPkgJson) {
388
+ log.warn('Nenhum package.json no diretório atual.');
389
+ const { create } = await inquirer.prompt([
390
+ {
391
+ type: 'confirm',
392
+ name: 'create',
393
+ message: 'Criar package.json básico aqui?',
394
+ default: true,
395
+ },
396
+ ]);
397
+ if (!create) {
398
+ log.dim('Cancellado. Navegue pro diretório do projeto primeiro.');
399
+ return;
400
+ }
401
+ const pkgContent = JSON.stringify({
402
+ name: path.basename(cwd).toLowerCase().replace(/[^a-z0-9-]/g, '-'),
403
+ version: '0.1.0',
404
+ type: 'module',
405
+ scripts: { dev: 'tsx src/index.ts' },
406
+ dependencies: {},
407
+ }, null, 2);
408
+ await fsp.writeFile(pkgJsonPath, pkgContent, 'utf8');
409
+ log.success('package.json criado.');
410
+ }
411
+ // Verifica se SDK já está presente
412
+ const pkgRaw = await fsp.readFile(pkgJsonPath, 'utf8');
413
+ const pkg = JSON.parse(pkgRaw);
414
+ const alreadyInstalled = pkg.dependencies?.[SDK_PACKAGE] ?? pkg.devDependencies?.[SDK_PACKAGE];
415
+ if (alreadyInstalled) {
416
+ log.info(`${SDK_PACKAGE} já presente: ${chalk.cyan(alreadyInstalled)}`);
417
+ }
418
+ else {
419
+ const spinner = ora({ text: `Instalando ${SDK_PACKAGE}@${SDK_VERSION}…`, color: 'cyan' }).start();
420
+ const r = spawnSync('npm', ['install', `${SDK_PACKAGE}@${SDK_VERSION}`], {
421
+ cwd,
422
+ encoding: 'utf8',
423
+ });
424
+ if (r.status !== 0) {
425
+ spinner.fail('Falha ao instalar @neetru/sdk.');
426
+ log.error((r.stderr ?? '').trim() || 'Verifique sua conexão com o npm.');
427
+ safeExit(1);
428
+ }
429
+ spinner.succeed(`${SDK_PACKAGE}@${SDK_VERSION} instalado.`);
430
+ }
431
+ // Cria boilerplate src/neetru.ts
432
+ const srcDir = path.join(cwd, 'src');
433
+ await ensureDir(srcDir);
434
+ const boilerplatePath = path.join(srcDir, 'neetru.ts');
435
+ const exists = await pathExists(boilerplatePath);
436
+ if (!exists) {
437
+ const boilerplate = [
438
+ '/**',
439
+ ' * neetru.ts — ponto de entrada do @neetru/sdk.',
440
+ ' *',
441
+ ' * Gerado por: neetru archive sdk init',
442
+ ' *',
443
+ " * Substitua os valores pelas suas variáveis de ambiente.",
444
+ " * Em modo dev (NEETRU_ENV=dev) o SDK usa mocks in-memory automaticamente.",
445
+ ' */',
446
+ "import { createNeetruClient } from '@neetru/sdk';",
447
+ '',
448
+ 'export const neetru = createNeetruClient({',
449
+ " apiKey: process.env.NEETRU_API_KEY ?? '',",
450
+ " env: (process.env.NEETRU_ENV as 'dev' | 'prod') ?? 'dev',",
451
+ '});',
452
+ '',
453
+ '// Exemplos de uso:',
454
+ '//',
455
+ '// const user = await neetru.auth.signIn({ email, password });',
456
+ '// const catalog = await neetru.catalog.list();',
457
+ '// const ok = await neetru.entitlements.check({ tenantId, feature: \'export\' });',
458
+ '// await neetru.telemetry.track({ event: \'user.signed_in\', tenantId });',
459
+ '',
460
+ ].join('\n');
461
+ await fsp.writeFile(boilerplatePath, boilerplate, 'utf8');
462
+ log.success(`Boilerplate criado: ${chalk.cyan('src/neetru.ts')}`);
463
+ }
464
+ else {
465
+ log.info('src/neetru.ts já existe — não sobrescrito.');
466
+ }
467
+ console.log();
468
+ log.heading('Próximos passos');
469
+ console.log(` ${chalk.dim('1.')} Adicione ao .env.local:`);
470
+ console.log(` ${chalk.cyan('NEETRU_API_KEY=nrt_<keyId>_<secret>')}`);
471
+ console.log(` ${chalk.cyan('NEETRU_ENV=dev')}`);
472
+ console.log(` ${chalk.dim('2.')} Importe onde precisar:`);
473
+ console.log(` ${chalk.cyan("import { neetru } from './src/neetru'")}`);
474
+ console.log(` ${chalk.dim('3.')} Docs: ${chalk.cyan('https://github.com/Neetru/neetru-sdk')}`);
475
+ console.log();
476
+ }
477
+ // ─── neetru archivesdk templates ──────────────────────────────────────────
478
+ export async function runSdkTemplates() {
479
+ const { fileURLToPath } = await import('node:url');
480
+ const { dirname, resolve } = await import('node:path');
481
+ const __fname = fileURLToPath(import.meta.url);
482
+ const __dir = dirname(__fname);
483
+ // Em dev (tsx): src/commands/ → ../templates
484
+ // Em prod (dist/): dist/commands/ → ../templates (não existe ali) → ../templates
485
+ const templatesDir = resolve(__dir, '..', 'templates');
486
+ const hasTemplates = await dirExists(templatesDir);
487
+ if (!hasTemplates) {
488
+ log.warn(`Diretório de templates não encontrado em: ${templatesDir}`);
489
+ return;
490
+ }
491
+ const entries = await fsp.readdir(templatesDir, { withFileTypes: true });
492
+ const templates = entries.filter((e) => e.isDirectory()).map((e) => e.name);
493
+ if (templates.length === 0) {
494
+ log.warn('Nenhum template disponível.');
495
+ return;
496
+ }
497
+ console.log();
498
+ log.heading('Templates SDK disponíveis');
499
+ for (const tmpl of templates) {
500
+ console.log(` ${chalk.cyan('·')} ${chalk.bold(tmpl)}`);
501
+ console.log(` ${chalk.dim(`neetru archive sdk add ${tmpl}`)}`);
502
+ }
503
+ console.log();
504
+ }
505
+ // ─── neetru archivesdk add <template> ─────────────────────────────────────
506
+ /**
507
+ * HIGH-4: templates permitidos são os que existem no diretório canônico.
508
+ * Lista estática reflete o conteúdo de /templates — atualizar quando novos
509
+ * templates forem adicionados ao pacote.
510
+ */
511
+ const ALLOWED_SDK_TEMPLATES = ['auth', 'billing', 'support', 'usage', 'users'];
512
+ export async function runSdkAddTemplate(template, opts = {}) {
513
+ // HIGH-4: rejeita nomes fora da whitelist antes de resolver qualquer path.
514
+ // Impede path traversal (ex: "../../etc/passwd") e templates não-canônicos.
515
+ if (!ALLOWED_SDK_TEMPLATES.includes(template)) {
516
+ log.error(`Template "${template}" não reconhecido.`);
517
+ log.dim(` Disponíveis: ${ALLOWED_SDK_TEMPLATES.join(', ')}`);
518
+ safeExit(1);
519
+ return;
520
+ }
521
+ const { fileURLToPath } = await import('node:url');
522
+ const { dirname, resolve, sep } = await import('node:path');
523
+ const __fname = fileURLToPath(import.meta.url);
524
+ const __dir = dirname(__fname);
525
+ const templatesDir = resolve(__dir, '..', 'templates');
526
+ // Defesa em profundidade: mesmo com whitelist, verifica que o path resolvido
527
+ // fica dentro de templatesDir (não pode sair por symlink ou encoding estranho).
528
+ const srcTemplate = resolve(templatesDir, template);
529
+ if (!srcTemplate.startsWith(resolve(templatesDir) + sep)) {
530
+ log.error('Caminho do template inválido.');
531
+ safeExit(1);
532
+ return;
533
+ }
534
+ if (!(await dirExists(srcTemplate))) {
535
+ log.error(`Template "${template}" não encontrado.`);
536
+ log.dim(' → Templates disponíveis: neetru archive sdk templates');
537
+ safeExit(1);
538
+ }
539
+ const cwd = process.cwd();
540
+ const destDir = path.join(cwd, 'src');
541
+ await ensureDir(destDir);
542
+ const entries = await fsp.readdir(srcTemplate, { withFileTypes: true });
543
+ let copied = 0;
544
+ for (const entry of entries) {
545
+ if (!entry.isFile())
546
+ continue;
547
+ const destPath = path.join(destDir, entry.name);
548
+ if (!opts.force && (await pathExists(destPath))) {
549
+ log.warn(`${entry.name} já existe — pulado (use --force para sobrescrever).`);
550
+ continue;
551
+ }
552
+ await fsp.copyFile(path.join(srcTemplate, entry.name), destPath);
553
+ console.log(` ${chalk.green('✓')} src/${entry.name}`);
554
+ copied++;
555
+ }
556
+ if (copied > 0) {
557
+ log.success(`Template "${template}" adicionado ao projeto.`);
558
+ }
559
+ else {
560
+ log.info('Nenhum arquivo novo copiado (todos já existiam).');
561
+ }
562
+ }
563
+ // ─── neetru archivebrowse ─────────────────────────────────────────────────
564
+ export async function runArchiveBrowse() {
565
+ const opener = resolveOpener();
566
+ if (!opener) {
567
+ log.info(`Acesse manualmente: ${chalk.cyan(LIBS_REPO_HTTPS)}`);
568
+ return;
569
+ }
570
+ const spinner = ora({ text: 'Abrindo browser…', color: 'cyan' }).start();
571
+ const r = spawnSync(opener, [LIBS_REPO_HTTPS], { stdio: 'ignore' });
572
+ if (r.status === 0 || r.status === null) {
573
+ spinner.succeed(`Aberto: ${chalk.cyan(LIBS_REPO_HTTPS)}`);
574
+ }
575
+ else {
576
+ spinner.stop();
577
+ log.info(`Acesse: ${chalk.cyan(LIBS_REPO_HTTPS)}`);
578
+ }
579
+ }
580
+ function resolveOpener() {
581
+ if (isWindows())
582
+ return 'explorer';
583
+ if (process.platform === 'darwin')
584
+ return 'open';
585
+ // Linux
586
+ const r = spawnSync('which', ['xdg-open'], { encoding: 'utf8', stdio: 'pipe' });
587
+ if (r.status === 0)
588
+ return 'xdg-open';
589
+ return null;
590
+ }
591
+ //# sourceMappingURL=archive.js.map