@soccagency/sh 0.2.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 ADDED
@@ -0,0 +1,242 @@
1
+ # Socc CLI
2
+
3
+ CLI para subir proyectos a Socc y obtener preview links instantáneos.
4
+
5
+ ## 🛠️ Stack
6
+
7
+ - **Runtime:** Node.js 20+
8
+ - **Package:** npm
9
+ - **Comprimiendo:** archiver
10
+ - **HTTP:** undici (fetch nativo)
11
+
12
+ ## 📋 Requisitos
13
+
14
+ - Node.js 20+
15
+ - npm o yarn
16
+ - Token de Socc (obtenido desde sh.socc.ink)
17
+
18
+ ## 🚀 Instalación
19
+
20
+ ### Desde npm (cuando esté publicado)
21
+
22
+ ```bash
23
+ npm install -g @socc/sh
24
+ ```
25
+
26
+ ### Desde código fuente
27
+
28
+ ```bash
29
+ # Clonar o navegar al directorio del CLI
30
+ cd /home/clawdbot/projects/socc/cli
31
+
32
+ # Instalar dependencias
33
+ npm install
34
+
35
+ # Instalar globalmente (desarrollo)
36
+ npm install -g .
37
+ ```
38
+
39
+ ## 🔧 Comandos
40
+
41
+ ### `socc login <token>`
42
+
43
+ Inicia sesión con tu API token.
44
+
45
+ ```bash
46
+ socc login socc_cw5MIU0haehbgAJma3vw4pXUqENHu8vj
47
+ ```
48
+
49
+ **Obtener tu token:**
50
+
51
+ 1. Andá a https://sh.socc.ink
52
+ 2. Logueate con GitHub
53
+ 3. Click en tu avatar → Settings
54
+ 4. Click en "Generar API Token"
55
+ 5. Copiá el token
56
+
57
+ ### `socc link [--ttl <horas>] [--framework <name>]`
58
+
59
+ Sube tu proyecto actual y crea un preview link.
60
+
61
+ ```bash
62
+ cd mi-proyecto
63
+ socc link
64
+ socc link --ttl 1
65
+ socc link --framework vite
66
+ ```
67
+
68
+ **Output:**
69
+ ```
70
+ 🚀 Socc - Deploy instantáneo
71
+
72
+ 🔐 Verificando tu cuenta...
73
+ ✅ Autenticado como: tu@email.com
74
+ 📊 Plan: FREE
75
+ 📈 Deployments activos: 0/1
76
+ 📅 Deployments hoy: 0/5
77
+ 📦 Framework usado: static
78
+ ⏰ TTL solicitado: 1 hora(s)
79
+ 📦 Comprimiendo archivos...
80
+ ✅ Archivos comprimidos
81
+ 📤 Subiendo a Socc...
82
+ ✅ Uploaded emqg9i7y/deploy.zip to R2 (0.00 MB)
83
+ ✅ Extracted 3 files to R2
84
+
85
+ ✅ ¡Deploy exitoso!
86
+
87
+ 🚀 Tu preview está en: https://emqg9i7y.socc.ink
88
+ ⏰ Expira en: 6 horas
89
+ ```
90
+
91
+ ### `socc list`
92
+
93
+ Lista tus deployments activos.
94
+
95
+ ```bash
96
+ socc list
97
+ ```
98
+
99
+ **Output:**
100
+ ```
101
+ 📋 Obteniendo tus deployments...
102
+
103
+ 📦 Tus deployments (2):
104
+
105
+ 1. 🟢 emqg9i7y
106
+ URL: https://emqg9i7y.socc.ink
107
+ Framework: static
108
+ Tamaño: 0.01 MB
109
+ Expira en: 5h
110
+
111
+ 2. 🟢 xQ39GogI
112
+ URL: https://xQ39GogI.socc.ink
113
+ Framework: react
114
+ Tamaño: 1.23 MB
115
+ Expira en: 23h
116
+ ```
117
+
118
+ ### `socc delete <id>`
119
+
120
+ Elimina un deployment.
121
+
122
+ ```bash
123
+ socc delete emqg9i7y
124
+ ```
125
+
126
+ **Output:**
127
+ ```
128
+ 🗑️ Borrando deployment emqg9i7y...
129
+ ✅ Deployment emqg9i7y eliminado.
130
+ ```
131
+
132
+ ### `socc whoami`
133
+
134
+ Muestra información de tu cuenta.
135
+
136
+ ```bash
137
+ socc whoami
138
+ ```
139
+
140
+ **Output:**
141
+ ```
142
+ 👤 Usuario: tu@email.com
143
+ 📊 Plan: FREE
144
+ 📈 Deployments activos: 0/1
145
+ 📅 Deployments hoy: 2/5
146
+ 📦 Total links: 15
147
+ ```
148
+
149
+ ## 📦 Frameworks Detectados
150
+
151
+ La CLI detecta automáticamente el framework y configura el build.
152
+ También detecta package manager (`npm`, `yarn`, `pnpm`, `bun`) por lockfile:
153
+
154
+ | Framework | Detecta | Build | Output |
155
+ |-----------|---------|-------|--------|
156
+ | **Vite** | `vite.config.js` | `npm run build` | `dist/` |
157
+ | **React (CRA)** | `package.json` + react-scripts | `npm run build` | `build/` |
158
+ | **Next.js** | `next.config.js` | `npm run build` | `out/` (static) |
159
+ | **Angular** | `angular.json` | `ng build` | `dist/` |
160
+ | **Astro** | `astro.config.*` o dependencia `astro` | `build` | `dist/` |
161
+ | **Static** | Por defecto | - | `.` |
162
+ | **Custom build** | `scripts.build` sin framework conocido | `build` | `dist/`, `build/` u `out/` |
163
+
164
+ Frameworks válidos para `--framework`:
165
+
166
+ `auto`, `vite`, `react`, `next`, `angular`, `nuxt`, `static`
167
+
168
+ > Para Next.js, Socc exige export estático (`out/`).
169
+ > Configurá `output: "export"` en `next.config.js`.
170
+
171
+ ## 🔍 Cómo funciona
172
+
173
+ 1. **Detecta el framework** buscando archivos de configuración
174
+ 2. **Ejecuta el build** si es necesario
175
+ 3. **Comprime** el output en un ZIP
176
+ 4. **Envía** el ZIP a la API (`POST /api/link`)
177
+ 5. **API** extrae y sube archivos a R2
178
+ 6. **Recibe** la URL del preview
179
+
180
+ ## 🐛 Troubleshooting
181
+
182
+ ### Error: "No estás autenticado"
183
+
184
+ ```bash
185
+ socc login <tu-token>
186
+ ```
187
+
188
+ ### Error: "Token inválido o expirado"
189
+
190
+ - El token puede haber sido revocado
191
+ - Generá uno nuevo en Settings
192
+
193
+ ### Error: "Llegaste al límite diario"
194
+
195
+ - Tu plan tiene un límite de links por día
196
+ - Esperá al próximo día o actualizá tu plan
197
+
198
+ ### Error: "Build fallido"
199
+
200
+ - Verificá que el proyecto compila localmente
201
+ - Revisá que tenés las dependencias instaladas
202
+
203
+ ### Error: "File too large"
204
+
205
+ - Tu plan tiene un límite de upload
206
+ - Free: 10 MB, Starter: 50 MB, Pro: 100 MB
207
+
208
+ ## 📝 Configuración
209
+
210
+ El CLI guarda tu token en:
211
+
212
+ ```
213
+ ~/.socc/config.json
214
+ ```
215
+
216
+ Contenido:
217
+ ```json
218
+ {
219
+ "token": "socc_cw5MIU0haehbgAJma3vw4pXUqENHu8vj"
220
+ }
221
+ ```
222
+
223
+ También podés cambiar la API con variable de entorno:
224
+
225
+ ```bash
226
+ SOCC_API_URL=http://localhost:3000 socc whoami
227
+ ```
228
+
229
+ ## 🚀 Publicar en npm
230
+
231
+ ```bash
232
+ # Publicar versión actual (ej: 0.2.0)
233
+ npm whoami
234
+ npm publish
235
+
236
+ # Verificar paquete antes de publicar
237
+ npm run pack:check
238
+ ```
239
+
240
+ ---
241
+
242
+ [Volver al README principal](../README.md)
package/bin/socc.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../src/index.js';
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@soccagency/sh",
3
+ "version": "0.2.0",
4
+ "description": "Instant preview links for your projects",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "socc": "bin/socc.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "pack:check": "npm pack --dry-run"
12
+ },
13
+ "files": [
14
+ "bin",
15
+ "src",
16
+ "README.md"
17
+ ],
18
+ "keywords": [
19
+ "deploy",
20
+ "preview",
21
+ "hosting",
22
+ "cli",
23
+ "tunnel"
24
+ ],
25
+ "author": "Socc Team",
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/GuilleG25/socc.git",
30
+ "directory": "cli"
31
+ },
32
+ "homepage": "https://sh.socc.ink",
33
+ "bugs": {
34
+ "url": "https://github.com/GuilleG25/socc/issues"
35
+ },
36
+ "engines": {
37
+ "node": ">=20"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "dependencies": {
43
+ "archiver": "^7.0.1",
44
+ "undici": "^7.22.0"
45
+ }
46
+ }
package/src/index.js ADDED
@@ -0,0 +1,689 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync } from 'child_process';
4
+ import { readFileSync, writeFileSync, existsSync, unlinkSync, mkdirSync } from 'fs';
5
+ import { join } from 'path';
6
+ import { createWriteStream } from 'fs';
7
+ import archiver from 'archiver';
8
+ import { fetch } from 'undici';
9
+
10
+ const API_URL = process.env.SOCC_API_URL || 'https://api.socc.ink';
11
+
12
+ const ALLOWED_FRAMEWORKS = ['auto', 'vite', 'react', 'next', 'angular', 'nuxt', 'astro', 'static'];
13
+
14
+ const FRAMEWORK_CONFIGS = {
15
+ vite: { name: 'vite', buildScript: 'build', distCandidates: ['dist'] },
16
+ react: { name: 'react', buildScript: 'build', distCandidates: ['build'] },
17
+ next: { name: 'next', buildScript: 'build', distCandidates: ['out'] },
18
+ angular: { name: 'angular', buildScript: 'build', distCandidates: ['dist'] },
19
+ nuxt: { name: 'nuxt', buildScript: 'generate', distCandidates: ['dist'] },
20
+ astro: { name: 'astro', buildScript: 'build', distCandidates: ['dist'] },
21
+ static: { name: 'static', buildScript: null, distCandidates: ['dist', '.'] }
22
+ };
23
+
24
+ // Colores
25
+ const colors = {
26
+ reset: '\x1b[0m',
27
+ red: '\x1b[31m',
28
+ green: '\x1b[32m',
29
+ yellow: '\x1b[33m',
30
+ blue: '\x1b[34m',
31
+ cyan: '\x1b[36m',
32
+ gray: '\x1b[90m',
33
+ white: '\x1b[37m'
34
+ };
35
+
36
+ function log(msg, color = 'reset') {
37
+ console.log(`${colors[color]}${msg}${colors.reset}`);
38
+ }
39
+
40
+ // ============= COMANDOS =============
41
+
42
+ function showHelp() {
43
+ log(`
44
+ 🚀 Socc CLI - Deploy instantáneo
45
+
46
+ Uso:
47
+ socc Mostrar esta ayuda
48
+ socc link [--ttl <horas>] [--framework <${ALLOWED_FRAMEWORKS.join('|')}>]
49
+ socc list Listar tus deployments
50
+ socc delete <id> Borrar un deployment
51
+ socc login <token> Iniciar sesión con API token
52
+ socc whoami Ver info de tu cuenta
53
+ socc help Mostrar esta ayuda
54
+
55
+ Flags de socc link:
56
+ --ttl <horas> TTL en horas (entero positivo, por defecto 6)
57
+ --framework <name> Fuerza framework: auto|vite|react|next|angular|nuxt|astro|static
58
+
59
+ Variables de entorno:
60
+ SOCC_API_URL API base URL (default: https://api.socc.ink)
61
+
62
+ Ejemplos:
63
+ $ socc login socc_abcdef123...
64
+ $ socc link --ttl 1
65
+ $ socc link --framework vite
66
+ $ SOCC_API_URL=http://localhost:3000 socc whoami
67
+
68
+ Planes:
69
+ Free - 1 deployment activo, 5/día, 10MB max
70
+ Starter - 5 deployments activos, 50/día, 50MB max
71
+ Pro - 20 deployments activos, 200/día, 100MB max
72
+ Enterprise - Ilimitado
73
+ `, 'cyan');
74
+ }
75
+
76
+ async function cmdLogin(args) {
77
+ const token = args[0];
78
+
79
+ if (!token) {
80
+ log('\n🔐 Socc Login\n', 'cyan');
81
+ log('Uso: socc login <tu-api-token>\n', 'white');
82
+ log('📋 Cómo obtener tu API token:', 'yellow');
83
+ log('1. Andá a https://sh.socc.ink y logueate con GitHub', 'yellow');
84
+ log('2. Hacé click en tu avatar → Settings', 'yellow');
85
+ log('3. Click en "Generar API Token"', 'yellow');
86
+ log('4. Copiá el token y ejecutá: socc login socc_xxxxx\n', 'yellow');
87
+ process.exit(0);
88
+ }
89
+
90
+ log('🔐 Verificando token...', 'yellow');
91
+ const userInfo = await getUserInfo(token);
92
+
93
+ if (!userInfo.success) {
94
+ log(`❌ Token inválido: ${userInfo.error}`, 'red');
95
+ process.exit(1);
96
+ }
97
+
98
+ // Guardar token
99
+ const configDir = join(process.env.HOME, '.socc');
100
+ const configPath = join(configDir, 'config.json');
101
+
102
+ if (!existsSync(configDir)) {
103
+ mkdirSync(configDir, { recursive: true });
104
+ }
105
+
106
+ writeFileSync(configPath, JSON.stringify({ token }, null, 2));
107
+
108
+ log('✅ ¡Login exitoso!', 'green');
109
+ log(`\n👤 Autenticado como: ${userInfo.user.email}`, 'white');
110
+ log(`📊 Plan: ${userInfo.user.tier.toUpperCase()}`, 'blue');
111
+ log('\n🚀 Ahora podés usar: socc link\n', 'cyan');
112
+ }
113
+
114
+ async function cmdWhoami() {
115
+ const token = getToken();
116
+
117
+ if (!token) {
118
+ log('❌ No estás autenticado. Corré `socc login` primero.', 'red');
119
+ process.exit(1);
120
+ }
121
+
122
+ const userInfo = await getUserInfo(token);
123
+
124
+ if (!userInfo.success) {
125
+ log(`❌ Error: ${userInfo.error}`, 'red');
126
+ process.exit(1);
127
+ }
128
+
129
+ const { user, usage } = userInfo;
130
+
131
+ log('\n👤 Tu cuenta Socc\n', 'cyan');
132
+ log(`Email: ${user.email}`, 'white');
133
+ log(`Plan: ${user.tier.toUpperCase()}`, 'blue');
134
+ log(`Estado: ${user.subscriptionStatus || 'active'}`, 'green');
135
+ log('\n📊 Uso:', 'yellow');
136
+ log(` Deployments activos: ${usage.activeDeployments}/${usage.maxActiveDeployments}`, 'white');
137
+ log(` Deployments hoy: ${usage.linksToday}/${usage.linksLimit}`, 'white');
138
+ log(` Max upload: ${usage.maxUpload}`, 'white');
139
+ log(` Max TTL: ${usage.maxTtl}`, 'white');
140
+ log(`\n📈 Total deployments: ${usage.totalLinks}\n`, 'gray');
141
+ }
142
+
143
+ async function cmdList() {
144
+ const token = getToken();
145
+
146
+ if (!token) {
147
+ log('❌ No estás autenticado. Corré `socc login` primero.', 'red');
148
+ process.exit(1);
149
+ }
150
+
151
+ log('📋 Obteniendo tus deployments...', 'yellow');
152
+
153
+ try {
154
+ const response = await fetch(`${API_URL}/api/list`, {
155
+ headers: { Authorization: `Bearer ${token}` }
156
+ });
157
+
158
+ const data = await response.json();
159
+
160
+ if (!data.success) {
161
+ log(`❌ Error: ${data.error}`, 'red');
162
+ process.exit(1);
163
+ }
164
+
165
+ if (data.deployments.length === 0) {
166
+ log('\n📭 No tenés deployments activos.', 'gray');
167
+ log('Creá tu primer deploy con: socc link\n', 'cyan');
168
+ return;
169
+ }
170
+
171
+ log('\n📦 Tus deployments:', 'cyan');
172
+
173
+ data.deployments.forEach((d, i) => {
174
+ const expires = new Date(d.expiresAt);
175
+ const now = new Date();
176
+ const hoursLeft = Math.round((expires - now) / (1000 * 60 * 60));
177
+ const status = hoursLeft > 0 ? '🟢' : '🔴';
178
+ const id = d.id || d.projectId || 'unknown';
179
+ const subdomain = d.subdomain || id;
180
+ const framework = d.framework || 'static';
181
+ const sizeMB = d.fileSize ? (d.fileSize / 1024 / 1024).toFixed(2) : '0.00';
182
+
183
+ console.log(` ${i + 1}. ${status} ${id}`);
184
+ console.log(` URL: https://${subdomain}.socc.ink`);
185
+ console.log(` Framework: ${framework}`);
186
+ console.log(` Tamaño: ${sizeMB} MB`);
187
+ console.log(` Expira en: ${hoursLeft > 0 ? `${hoursLeft}h` : 'EXPIRADO'}`);
188
+ console.log('');
189
+ });
190
+
191
+ } catch (error) {
192
+ log(`❌ Error: ${error.message}`, 'red');
193
+ process.exit(1);
194
+ }
195
+ }
196
+
197
+ async function cmdDelete(args) {
198
+ const projectId = args[0];
199
+
200
+ if (!projectId) {
201
+ log('\n❌ Debes especificar un ID de deployment', 'red');
202
+ log('\nUso: socc delete <project-id>\n', 'yellow');
203
+ log('Obtené el ID con: socc list\n', 'cyan');
204
+ process.exit(1);
205
+ }
206
+
207
+ const token = getToken();
208
+
209
+ if (!token) {
210
+ log('❌ No estás autenticado. Corré `socc login` primero.', 'red');
211
+ process.exit(1);
212
+ }
213
+
214
+ log(`🗑️ Borrando deployment ${projectId}...`, 'yellow');
215
+
216
+ try {
217
+ const response = await fetch(`${API_URL}/api/delete/${projectId}`, {
218
+ method: 'DELETE',
219
+ headers: { Authorization: `Bearer ${token}` }
220
+ });
221
+
222
+ const data = await response.json();
223
+
224
+ if (!data.success) {
225
+ log(`❌ Error: ${data.error}`, 'red');
226
+ process.exit(1);
227
+ }
228
+
229
+ log(`✅ Deployment ${projectId} eliminado.\n`, 'green');
230
+
231
+ } catch (error) {
232
+ log(`❌ Error: ${error.message}`, 'red');
233
+ process.exit(1);
234
+ }
235
+ }
236
+
237
+ async function cmdLink(args) {
238
+ const parsedOptions = parseLinkOptions(args);
239
+
240
+ if (!parsedOptions.ok) {
241
+ log(`❌ ${parsedOptions.error}`, 'red');
242
+ log(`\nUso: socc link [--ttl <horas>] [--framework <${ALLOWED_FRAMEWORKS.join('|')}>]\n`, 'yellow');
243
+ process.exit(1);
244
+ }
245
+
246
+ const options = parsedOptions.options;
247
+
248
+ log('🚀 Socc - Deploy instantáneo\n', 'cyan');
249
+
250
+ const token = getToken();
251
+
252
+ if (!token) {
253
+ log('❌ No estás autenticado. Corré `socc login` primero.', 'red');
254
+ log('\nPara loguearte: socc login socc_xxxxx\n', 'cyan');
255
+ process.exit(1);
256
+ }
257
+
258
+ // Verificar límites
259
+ log('🔐 Verificando tu cuenta...', 'yellow');
260
+ const userInfo = await getUserInfo(token);
261
+
262
+ if (!userInfo.success) {
263
+ log(`❌ Error: ${userInfo.error}`, 'red');
264
+ process.exit(1);
265
+ }
266
+
267
+ const { user, usage, canCreateLink } = userInfo;
268
+
269
+ log(`✅ Autenticado como: ${user.email}`, 'green');
270
+ log(`📊 Plan: ${user.tier.toUpperCase()}`, 'blue');
271
+ log(`📈 Deployments activos: ${usage.activeDeployments}/${usage.maxActiveDeployments}`, 'blue');
272
+ log(`📅 Deployments hoy: ${usage.linksToday}/${usage.linksLimit}`, 'blue');
273
+
274
+ if (!canCreateLink) {
275
+ log('\n❌ Llegaste al límite diario de deployments.', 'red');
276
+ log(`Tu plan (${user.tier}) permite ${usage.linksLimit} deployments por día.`, 'yellow');
277
+ log('Volvé mañana o actualizá tu plan.\n', 'yellow');
278
+ process.exit(1);
279
+ }
280
+
281
+ if (usage.activeDeployments >= usage.maxActiveDeployments && usage.maxActiveDeployments !== 'unlimited') {
282
+ log('\n❌ Llegaste al máximo de deployments activos.', 'red');
283
+ log(`Tu plan (${user.tier}) permite ${usage.maxActiveDeployments} deployments activos.`, 'yellow');
284
+ log('Eliminá previews viejos con: socc delete <id>', 'yellow');
285
+ log('O actualizá tu plan.\n', 'yellow');
286
+ process.exit(1);
287
+ }
288
+
289
+ // Detectar framework
290
+ const framework = detectFramework(options.framework);
291
+ const packageManager = detectPackageManager();
292
+ log(`📦 Framework usado: ${framework.name}`, 'blue');
293
+ log(`📦 Package manager detectado: ${packageManager}`, 'blue');
294
+ log(`⏰ TTL solicitado: ${options.ttl} hora(s)`, 'blue');
295
+
296
+ // Build
297
+ if (framework.buildScript) {
298
+ const buildCommand = buildCommandForScript(packageManager, framework.buildScript);
299
+ log(`🔨 Construyendo proyecto (${buildCommand})...`, 'yellow');
300
+ try {
301
+ execSync(buildCommand, { stdio: 'inherit' });
302
+ log('✅ Build completado', 'green');
303
+ } catch {
304
+ log('❌ Build fallido', 'red');
305
+ process.exit(1);
306
+ }
307
+ }
308
+
309
+ let distPath;
310
+ try {
311
+ distPath = resolveDistPath(framework);
312
+ } catch (error) {
313
+ log(`❌ ${error.message}`, 'red');
314
+ process.exit(1);
315
+ }
316
+
317
+ // Comprimir
318
+ log('📦 Comprimiendo archivos...', 'yellow');
319
+ const zipPath = await zipDirectory(distPath);
320
+ log('✅ Archivos comprimidos', 'green');
321
+
322
+ // Upload
323
+ log('📤 Subiendo a Socc...', 'yellow');
324
+ try {
325
+ const result = await uploadToApi(zipPath, token, {
326
+ ttl: options.ttl,
327
+ framework: framework.name
328
+ });
329
+
330
+ if (result.success && result.url) {
331
+ log('\n✅ ¡Deploy exitoso!', 'green');
332
+ log(`\n🚀 Tu preview está en: ${result.url}\n`, 'cyan');
333
+ log(`⏰ Expira en: ${result.ttl} horas\n`, 'blue');
334
+ } else {
335
+ log('❌ Error en el deploy', 'red');
336
+ if (result.error) {
337
+ log(result.error, 'red');
338
+ }
339
+ process.exit(1);
340
+ }
341
+ } catch (error) {
342
+ log(`❌ Error: ${error.message}`, 'red');
343
+ process.exit(1);
344
+ }
345
+
346
+ // Limpiar
347
+ try {
348
+ unlinkSync(zipPath);
349
+ } catch {
350
+ // ignore cleanup errors
351
+ }
352
+ }
353
+
354
+ // ============= HELPERS =============
355
+
356
+ function getToken() {
357
+ const configPath = join(process.env.HOME, '.socc', 'config.json');
358
+ if (!existsSync(configPath)) {
359
+ return null;
360
+ }
361
+
362
+ try {
363
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
364
+ return config.token;
365
+ } catch {
366
+ return null;
367
+ }
368
+ }
369
+
370
+ async function getUserInfo(token) {
371
+ try {
372
+ const response = await fetch(`${API_URL}/api/whoami`, {
373
+ headers: { Authorization: `Bearer ${token}` }
374
+ });
375
+
376
+ if (response.status === 401) {
377
+ return { success: false, error: 'Token inválido o expirado' };
378
+ }
379
+
380
+ const data = await response.json();
381
+
382
+ if (!data || !data.success) {
383
+ return { success: false, error: data?.error || 'Error al verificar usuario' };
384
+ }
385
+
386
+ return {
387
+ success: true,
388
+ user: data.user || {},
389
+ usage: data.usage || {},
390
+ limits: data.limits || {},
391
+ canCreateLink: data.canCreateLink !== false
392
+ };
393
+ } catch (error) {
394
+ return { success: false, error: error.message || 'Error de conexión' };
395
+ }
396
+ }
397
+
398
+ function parseLinkOptions(args) {
399
+ let ttl = 6;
400
+ let framework = 'auto';
401
+
402
+ for (let i = 0; i < args.length; i += 1) {
403
+ const arg = args[i];
404
+
405
+ if (arg === '--ttl') {
406
+ const ttlValue = args[i + 1];
407
+ if (!ttlValue) {
408
+ return { ok: false, error: 'Falta valor para --ttl' };
409
+ }
410
+
411
+ const parsedTtl = Number.parseInt(ttlValue, 10);
412
+ if (!Number.isInteger(parsedTtl) || parsedTtl <= 0) {
413
+ return { ok: false, error: '--ttl debe ser un entero positivo' };
414
+ }
415
+
416
+ ttl = parsedTtl;
417
+ i += 1;
418
+ continue;
419
+ }
420
+
421
+ if (arg.startsWith('--ttl=')) {
422
+ const ttlValue = arg.split('=')[1];
423
+ const parsedTtl = Number.parseInt(ttlValue, 10);
424
+ if (!Number.isInteger(parsedTtl) || parsedTtl <= 0) {
425
+ return { ok: false, error: '--ttl debe ser un entero positivo' };
426
+ }
427
+
428
+ ttl = parsedTtl;
429
+ continue;
430
+ }
431
+
432
+ if (arg === '--framework') {
433
+ const frameworkValue = args[i + 1];
434
+ if (!frameworkValue) {
435
+ return { ok: false, error: 'Falta valor para --framework' };
436
+ }
437
+
438
+ if (!ALLOWED_FRAMEWORKS.includes(frameworkValue)) {
439
+ return { ok: false, error: `Framework inválido: ${frameworkValue}` };
440
+ }
441
+
442
+ framework = frameworkValue;
443
+ i += 1;
444
+ continue;
445
+ }
446
+
447
+ if (arg.startsWith('--framework=')) {
448
+ const frameworkValue = arg.split('=')[1];
449
+ if (!ALLOWED_FRAMEWORKS.includes(frameworkValue)) {
450
+ return { ok: false, error: `Framework inválido: ${frameworkValue}` };
451
+ }
452
+
453
+ framework = frameworkValue;
454
+ continue;
455
+ }
456
+
457
+ return { ok: false, error: `Flag no soportada: ${arg}` };
458
+ }
459
+
460
+ return {
461
+ ok: true,
462
+ options: {
463
+ ttl,
464
+ framework
465
+ }
466
+ };
467
+ }
468
+
469
+ function detectFramework(preferredFramework = 'auto') {
470
+ if (preferredFramework !== 'auto') {
471
+ return { ...FRAMEWORK_CONFIGS[preferredFramework] };
472
+ }
473
+
474
+ const pkgPath = join(process.cwd(), 'package.json');
475
+
476
+ if (!existsSync(pkgPath)) {
477
+ if (existsSync(join(process.cwd(), 'index.html'))) {
478
+ return { ...FRAMEWORK_CONFIGS.static, distCandidates: ['.'] };
479
+ }
480
+
481
+ log('❌ No se encontró package.json. Si es un sitio estático, incluí un index.html o usá --framework static.', 'red');
482
+ process.exit(1);
483
+ }
484
+
485
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
486
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
487
+
488
+ if (deps.vite || hasAnyFile(['vite.config.js', 'vite.config.mjs', 'vite.config.ts', 'vite.config.cjs'])) {
489
+ return { ...FRAMEWORK_CONFIGS.vite };
490
+ }
491
+
492
+ if (deps.next || hasAnyFile(['next.config.js', 'next.config.mjs', 'next.config.ts'])) {
493
+ return { ...FRAMEWORK_CONFIGS.next };
494
+ }
495
+
496
+ if (deps['react-scripts']) {
497
+ return { ...FRAMEWORK_CONFIGS.react };
498
+ }
499
+
500
+ if (deps['@angular/core'] || existsSync(join(process.cwd(), 'angular.json'))) {
501
+ return { ...FRAMEWORK_CONFIGS.angular };
502
+ }
503
+
504
+ if (deps.nuxt || hasAnyFile(['nuxt.config.js', 'nuxt.config.ts'])) {
505
+ return { ...FRAMEWORK_CONFIGS.nuxt };
506
+ }
507
+
508
+ if (deps.astro || hasAnyFile(['astro.config.mjs', 'astro.config.js', 'astro.config.ts', 'astro.config.cjs'])) {
509
+ return { ...FRAMEWORK_CONFIGS.astro };
510
+ }
511
+
512
+ if (pkg.scripts?.build) {
513
+ return { name: 'custom', buildScript: 'build', distCandidates: ['dist', 'build', 'out'] };
514
+ }
515
+
516
+ const existingOutputDir = findFirstExistingDirectory(['dist', 'build', 'out']);
517
+ if (existingOutputDir) {
518
+ return { ...FRAMEWORK_CONFIGS.static, distCandidates: [existingOutputDir, '.'] };
519
+ }
520
+
521
+ if (existsSync(join(process.cwd(), 'index.html'))) {
522
+ return { ...FRAMEWORK_CONFIGS.static, distCandidates: ['.'] };
523
+ }
524
+
525
+ log('❌ No se pudo detectar framework ni directorio de salida. Usá --framework o creá script build.', 'red');
526
+ process.exit(1);
527
+ }
528
+
529
+ function hasAnyFile(fileNames) {
530
+ return fileNames.some((fileName) => existsSync(join(process.cwd(), fileName)));
531
+ }
532
+
533
+ function findFirstExistingDirectory(candidates) {
534
+ for (const candidate of candidates) {
535
+ if (existsSync(join(process.cwd(), candidate))) {
536
+ return candidate;
537
+ }
538
+ }
539
+
540
+ return null;
541
+ }
542
+
543
+ function detectPackageManager() {
544
+ if (existsSync(join(process.cwd(), 'bun.lockb')) || existsSync(join(process.cwd(), 'bun.lock'))) {
545
+ return 'bun';
546
+ }
547
+
548
+ if (existsSync(join(process.cwd(), 'pnpm-lock.yaml'))) {
549
+ return 'pnpm';
550
+ }
551
+
552
+ if (existsSync(join(process.cwd(), 'yarn.lock'))) {
553
+ return 'yarn';
554
+ }
555
+
556
+ if (existsSync(join(process.cwd(), 'package-lock.json'))) {
557
+ return 'npm';
558
+ }
559
+
560
+ return 'npm';
561
+ }
562
+
563
+ function buildCommandForScript(packageManager, scriptName) {
564
+ if (packageManager === 'yarn') {
565
+ return `yarn ${scriptName}`;
566
+ }
567
+
568
+ if (packageManager === 'pnpm') {
569
+ return `pnpm ${scriptName}`;
570
+ }
571
+
572
+ if (packageManager === 'bun') {
573
+ return `bun run ${scriptName}`;
574
+ }
575
+
576
+ return `npm run ${scriptName}`;
577
+ }
578
+
579
+ function resolveDistPath(framework) {
580
+ for (const candidate of framework.distCandidates) {
581
+ if (candidate === '.') {
582
+ if (existsSync(join(process.cwd(), 'index.html'))) {
583
+ return process.cwd();
584
+ }
585
+
586
+ continue;
587
+ }
588
+
589
+ const distPath = join(process.cwd(), candidate);
590
+
591
+ if (existsSync(distPath)) {
592
+ return distPath;
593
+ }
594
+ }
595
+
596
+ if (framework.name === 'next') {
597
+ throw new Error('No se encontró el directorio "out" de Next.js. Configurá `output: "export"` en next.config.js para export estático y volvé a ejecutar el build.');
598
+ }
599
+
600
+ if (framework.name === 'custom') {
601
+ throw new Error('Build ejecutado, pero no se encontró salida en dist/, build/ u out/. Forzá --framework o ajustá tu output.');
602
+ }
603
+
604
+ throw new Error(`No se encontró el directorio de salida (${framework.distCandidates.join(', ')})`);
605
+ }
606
+
607
+ async function zipDirectory(dirPath) {
608
+ const zipPath = join(process.cwd(), 'socc-deploy.zip');
609
+
610
+ return new Promise((resolve, reject) => {
611
+ const output = createWriteStream(zipPath);
612
+ const archive = archiver('zip', { zlib: { level: 9 } });
613
+
614
+ output.on('close', () => resolve(zipPath));
615
+ archive.on('error', reject);
616
+
617
+ archive.pipe(output);
618
+ archive.directory(dirPath, false, (entry) => {
619
+ if (
620
+ entry.name.includes('node_modules')
621
+ || entry.name.includes('.git')
622
+ || entry.name.includes('.next')
623
+ || entry.name === 'socc-deploy.zip'
624
+ ) {
625
+ return false;
626
+ }
627
+ return entry;
628
+ });
629
+
630
+ archive.finalize();
631
+ });
632
+ }
633
+
634
+ async function uploadToApi(zipPath, token, options) {
635
+ const fileBuffer = readFileSync(zipPath);
636
+
637
+ const response = await fetch(`${API_URL}/api/link`, {
638
+ method: 'POST',
639
+ headers: {
640
+ Authorization: `Bearer ${token}`,
641
+ 'Content-Type': 'application/zip',
642
+ 'X-TTL': `${options.ttl}`,
643
+ 'X-Framework': options.framework
644
+ },
645
+ body: new Uint8Array(fileBuffer)
646
+ });
647
+
648
+ return response.json();
649
+ }
650
+
651
+ // ============= MAIN =============
652
+
653
+ async function main() {
654
+ const args = process.argv.slice(2);
655
+ const command = args[0];
656
+
657
+ switch (command) {
658
+ case undefined:
659
+ case 'help':
660
+ case '--help':
661
+ case '-h':
662
+ showHelp();
663
+ break;
664
+ case 'login':
665
+ await cmdLogin(args.slice(1));
666
+ break;
667
+ case 'whoami':
668
+ await cmdWhoami();
669
+ break;
670
+ case 'link':
671
+ await cmdLink(args.slice(1));
672
+ break;
673
+ case 'list':
674
+ await cmdList();
675
+ break;
676
+ case 'delete':
677
+ await cmdDelete(args.slice(1));
678
+ break;
679
+ default:
680
+ log(`❌ Comando desconocido: ${command}`, 'red');
681
+ log('Usá `socc help` para ver los comandos disponibles', 'yellow');
682
+ process.exit(1);
683
+ }
684
+ }
685
+
686
+ main().catch((error) => {
687
+ console.error(error);
688
+ process.exit(1);
689
+ });