@soccagency/sh 0.2.0 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +54 -188
  2. package/package.json +1 -1
  3. package/src/index.js +128 -107
package/README.md CHANGED
@@ -1,242 +1,108 @@
1
1
  # Socc CLI
2
2
 
3
- CLI para subir proyectos a Socc y obtener preview links instantáneos.
3
+ CLI for publishing instant preview links with Socc.
4
4
 
5
- ## 🛠️ Stack
6
-
7
- - **Runtime:** Node.js 20+
8
- - **Package:** npm
9
- - **Comprimiendo:** archiver
10
- - **HTTP:** undici (fetch nativo)
11
-
12
- ## 📋 Requisitos
5
+ ## Requirements
13
6
 
14
7
  - Node.js 20+
15
- - npm o yarn
16
- - Token de Socc (obtenido desde sh.socc.ink)
8
+ - npm account + API token from `https://sh.socc.ink`
17
9
 
18
- ## 🚀 Instalación
10
+ ## Install
19
11
 
20
- ### Desde npm (cuando esté publicado)
12
+ ### From npm (public package)
21
13
 
22
14
  ```bash
23
- npm install -g @socc/sh
15
+ npm i -g @soccagency/sh
24
16
  ```
25
17
 
26
- ### Desde código fuente
18
+ ### From source (local development)
27
19
 
28
20
  ```bash
29
- # Clonar o navegar al directorio del CLI
30
21
  cd /home/clawdbot/projects/socc/cli
31
-
32
- # Instalar dependencias
33
22
  npm install
34
-
35
- # Instalar globalmente (desarrollo)
36
23
  npm install -g .
37
24
  ```
38
25
 
39
- ## 🔧 Comandos
40
-
41
- ### `socc login <token>`
42
-
43
- Inicia sesión con tu API token.
26
+ ## Quickstart
44
27
 
45
28
  ```bash
46
- socc login socc_cw5MIU0haehbgAJma3vw4pXUqENHu8vj
47
- ```
29
+ # 1) Authenticate once
30
+ socc login socc_xxxxx
48
31
 
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
32
+ # 2) Deploy from your project folder
63
33
  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
34
 
91
- ### `socc list`
35
+ # 3) Optional: set TTL and framework explicitly
36
+ socc link --ttl 1 --framework vite
92
37
 
93
- Lista tus deployments activos.
94
-
95
- ```bash
38
+ # 4) List and delete previews
96
39
  socc list
40
+ socc delete <deployment-id>
97
41
  ```
98
42
 
99
- **Output:**
100
- ```
101
- 📋 Obteniendo tus deployments...
43
+ ## Commands
102
44
 
103
- 📦 Tus deployments (2):
45
+ - `socc login <token>`: Save API token locally.
46
+ - `socc logout`: Remove saved API token.
47
+ - `socc whoami`: Show account plan and usage limits.
48
+ - `socc link [--ttl <hours>] [--framework <name>]`: Build, package, and deploy.
49
+ - `socc list`: List active deployments.
50
+ - `socc delete <id>`: Hard-delete a deployment.
51
+ - `socc help`: Show command help.
104
52
 
105
- 1. 🟢 emqg9i7y
106
- URL: https://emqg9i7y.socc.ink
107
- Framework: static
108
- Tamaño: 0.01 MB
109
- Expira en: 5h
53
+ ## Link Options
110
54
 
111
- 2. 🟢 xQ39GogI
112
- URL: https://xQ39GogI.socc.ink
113
- Framework: react
114
- Tamaño: 1.23 MB
115
- Expira en: 23h
116
- ```
55
+ - `--ttl <hours>`: Positive integer in hours. Default is `6`.
56
+ - `--framework <name>`: `auto|vite|react|next|angular|nuxt|astro|static`.
117
57
 
118
- ### `socc delete <id>`
58
+ If you do not pass `--framework`, detection runs automatically.
119
59
 
120
- Elimina un deployment.
60
+ ## Auto Detection
121
61
 
122
- ```bash
123
- socc delete emqg9i7y
124
- ```
62
+ The CLI detects:
125
63
 
126
- **Output:**
127
- ```
128
- 🗑️ Borrando deployment emqg9i7y...
129
- ✅ Deployment emqg9i7y eliminado.
130
- ```
64
+ - Framework: `vite`, `react`, `next`, `angular`, `nuxt`, `astro`, `static`, or custom build script.
65
+ - Package manager: `npm`, `yarn`, `pnpm`, or `bun` (by lockfile).
131
66
 
132
- ### `socc whoami`
67
+ ### Next.js note
133
68
 
134
- Muestra información de tu cuenta.
69
+ Socc requires static export output (`out/`) for Next.js.
135
70
 
136
- ```bash
137
- socc whoami
138
- ```
71
+ Set this in `next.config.js`:
139
72
 
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
73
+ ```js
74
+ export default {
75
+ output: 'export'
76
+ };
147
77
  ```
148
78
 
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:
79
+ Then run `socc link` again.
153
80
 
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/` |
81
+ ## Environment Variable
163
82
 
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"
83
+ You can override the API URL:
183
84
 
184
85
  ```bash
185
- socc login <tu-token>
86
+ SOCC_API_URL=http://localhost:3000 socc whoami
186
87
  ```
187
88
 
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
89
+ Default: `https://api.socc.ink`
207
90
 
208
- ## 📝 Configuración
91
+ ## Auth Storage
209
92
 
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:
93
+ Token is saved in:
224
94
 
225
95
  ```bash
226
- SOCC_API_URL=http://localhost:3000 socc whoami
96
+ ~/.socc/config.json
227
97
  ```
228
98
 
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
- ```
99
+ - Switch account: run `socc login <new-token>` (replaces current token).
100
+ - Logout: run `socc logout`.
239
101
 
240
- ---
102
+ ## Troubleshooting
241
103
 
242
- [Volver al README principal](../README.md)
104
+ - `You are not authenticated`: run `socc login <token>`.
105
+ - `Token is invalid or expired`: generate a new token in `https://sh.socc.ink`.
106
+ - `Daily deployment limit reached`: wait for reset or upgrade plan.
107
+ - `Build failed`: verify local build command works before running `socc link`.
108
+ - `Output directory not found`: set framework explicitly or fix build output path.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soccagency/sh",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Instant preview links for your projects",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -21,7 +21,7 @@ const FRAMEWORK_CONFIGS = {
21
21
  static: { name: 'static', buildScript: null, distCandidates: ['dist', '.'] }
22
22
  };
23
23
 
24
- // Colores
24
+ // Colors
25
25
  const colors = {
26
26
  reset: '\x1b[0m',
27
27
  red: '\x1b[31m',
@@ -37,39 +37,40 @@ function log(msg, color = 'reset') {
37
37
  console.log(`${colors[color]}${msg}${colors.reset}`);
38
38
  }
39
39
 
40
- // ============= COMANDOS =============
40
+ // ============= COMMANDS =============
41
41
 
42
42
  function showHelp() {
43
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:
44
+ 🚀 Socc CLI - Instant Deploy
45
+
46
+ Usage:
47
+ socc Show help
48
+ socc link [--ttl <hours>] [--framework <${ALLOWED_FRAMEWORKS.join('|')}>]
49
+ socc list List deployments
50
+ socc delete <id> Delete deployment
51
+ socc login <token> Authenticate with API token
52
+ socc logout Remove saved API token
53
+ socc whoami Show account info
54
+ socc help Show help
55
+
56
+ Link flags:
57
+ --ttl <hours> TTL in hours (positive integer, default 6)
58
+ --framework <name> Force framework: auto|vite|react|next|angular|nuxt|astro|static
59
+
60
+ Environment variables:
60
61
  SOCC_API_URL API base URL (default: https://api.socc.ink)
61
62
 
62
- Ejemplos:
63
+ Examples:
63
64
  $ socc login socc_abcdef123...
64
65
  $ socc link --ttl 1
65
66
  $ socc link --framework vite
66
67
  $ SOCC_API_URL=http://localhost:3000 socc whoami
67
68
 
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
69
+ Plans:
70
+ Free - 1 active deployment, 5/day, 10MB max
71
+ Starter - 5 active deployments, 50/day, 50MB max
72
+ Pro - 20 active deployments, 200/day, 100MB max
73
+ Enterprise - Unlimited
73
74
  `, 'cyan');
74
75
  }
75
76
 
@@ -78,24 +79,24 @@ async function cmdLogin(args) {
78
79
 
79
80
  if (!token) {
80
81
  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');
82
+ log('Usage: socc login <your-api-token>\n', 'white');
83
+ log('📋 How to get your API token:', 'yellow');
84
+ log('1. Go to https://sh.socc.ink and sign in with GitHub', 'yellow');
85
+ log('2. Open your account settings', 'yellow');
86
+ log('3. Click "Generate API token"', 'yellow');
87
+ log('4. Copy the token and run: socc login socc_xxxxx\n', 'yellow');
87
88
  process.exit(0);
88
89
  }
89
90
 
90
- log('🔐 Verificando token...', 'yellow');
91
+ log('🔐 Verifying token...', 'yellow');
91
92
  const userInfo = await getUserInfo(token);
92
93
 
93
94
  if (!userInfo.success) {
94
- log(`❌ Token inválido: ${userInfo.error}`, 'red');
95
+ log(`❌ Invalid token: ${userInfo.error}`, 'red');
95
96
  process.exit(1);
96
97
  }
97
98
 
98
- // Guardar token
99
+ // Save token
99
100
  const configDir = join(process.env.HOME, '.socc');
100
101
  const configPath = join(configDir, 'config.json');
101
102
 
@@ -105,17 +106,17 @@ async function cmdLogin(args) {
105
106
 
106
107
  writeFileSync(configPath, JSON.stringify({ token }, null, 2));
107
108
 
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');
109
+ log('✅ Login successful!', 'green');
110
+ log(`\n👤 Authenticated as: ${userInfo.user.email}`, 'white');
111
+ log(`📊 Tier: ${userInfo.user.tier.toUpperCase()}`, 'blue');
112
+ log('\n🚀 You can now run: socc link\n', 'cyan');
112
113
  }
113
114
 
114
115
  async function cmdWhoami() {
115
116
  const token = getToken();
116
117
 
117
118
  if (!token) {
118
- log('❌ No estás autenticado. Corré `socc login` primero.', 'red');
119
+ log('❌ You are not authenticated. Run `socc login` first.', 'red');
119
120
  process.exit(1);
120
121
  }
121
122
 
@@ -128,13 +129,13 @@ async function cmdWhoami() {
128
129
 
129
130
  const { user, usage } = userInfo;
130
131
 
131
- log('\n👤 Tu cuenta Socc\n', 'cyan');
132
+ log('\n👤 Your Socc account\n', 'cyan');
132
133
  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');
134
+ log(`Tier: ${user.tier.toUpperCase()}`, 'blue');
135
+ log(`Status: ${user.subscriptionStatus || 'active'}`, 'green');
136
+ log('\n📊 Usage:', 'yellow');
137
+ log(` Active deployments: ${usage.activeDeployments}/${usage.maxActiveDeployments}`, 'white');
138
+ log(` Deployments today: ${usage.linksToday}/${usage.linksLimit}`, 'white');
138
139
  log(` Max upload: ${usage.maxUpload}`, 'white');
139
140
  log(` Max TTL: ${usage.maxTtl}`, 'white');
140
141
  log(`\n📈 Total deployments: ${usage.totalLinks}\n`, 'gray');
@@ -144,11 +145,11 @@ async function cmdList() {
144
145
  const token = getToken();
145
146
 
146
147
  if (!token) {
147
- log('❌ No estás autenticado. Corré `socc login` primero.', 'red');
148
+ log('❌ You are not authenticated. Run `socc login` first.', 'red');
148
149
  process.exit(1);
149
150
  }
150
151
 
151
- log('📋 Obteniendo tus deployments...', 'yellow');
152
+ log('📋 Fetching your deployments...', 'yellow');
152
153
 
153
154
  try {
154
155
  const response = await fetch(`${API_URL}/api/list`, {
@@ -163,12 +164,12 @@ async function cmdList() {
163
164
  }
164
165
 
165
166
  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');
167
+ log('\n📭 No active deployments found.', 'gray');
168
+ log('Create your first deploy with: socc link\n', 'cyan');
168
169
  return;
169
170
  }
170
171
 
171
- log('\n📦 Tus deployments:', 'cyan');
172
+ log('\n📦 Your deployments:', 'cyan');
172
173
 
173
174
  data.deployments.forEach((d, i) => {
174
175
  const expires = new Date(d.expiresAt);
@@ -183,8 +184,8 @@ async function cmdList() {
183
184
  console.log(` ${i + 1}. ${status} ${id}`);
184
185
  console.log(` URL: https://${subdomain}.socc.ink`);
185
186
  console.log(` Framework: ${framework}`);
186
- console.log(` Tamaño: ${sizeMB} MB`);
187
- console.log(` Expira en: ${hoursLeft > 0 ? `${hoursLeft}h` : 'EXPIRADO'}`);
187
+ console.log(` Size: ${sizeMB} MB`);
188
+ console.log(` Expires in: ${hoursLeft > 0 ? `${hoursLeft}h` : 'EXPIRED'}`);
188
189
  console.log('');
189
190
  });
190
191
 
@@ -198,20 +199,20 @@ async function cmdDelete(args) {
198
199
  const projectId = args[0];
199
200
 
200
201
  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');
202
+ log('\n❌ You must provide a deployment ID.', 'red');
203
+ log('\nUsage: socc delete <project-id>\n', 'yellow');
204
+ log('Get the ID with: socc list\n', 'cyan');
204
205
  process.exit(1);
205
206
  }
206
207
 
207
208
  const token = getToken();
208
209
 
209
210
  if (!token) {
210
- log('❌ No estás autenticado. Corré `socc login` primero.', 'red');
211
+ log('❌ You are not authenticated. Run `socc login` first.', 'red');
211
212
  process.exit(1);
212
213
  }
213
214
 
214
- log(`🗑️ Borrando deployment ${projectId}...`, 'yellow');
215
+ log(`🗑️ Deleting deployment ${projectId}...`, 'yellow');
215
216
 
216
217
  try {
217
218
  const response = await fetch(`${API_URL}/api/delete/${projectId}`, {
@@ -226,7 +227,7 @@ async function cmdDelete(args) {
226
227
  process.exit(1);
227
228
  }
228
229
 
229
- log(`✅ Deployment ${projectId} eliminado.\n`, 'green');
230
+ log(`✅ Deployment ${projectId} deleted.\n`, 'green');
230
231
 
231
232
  } catch (error) {
232
233
  log(`❌ Error: ${error.message}`, 'red');
@@ -239,24 +240,24 @@ async function cmdLink(args) {
239
240
 
240
241
  if (!parsedOptions.ok) {
241
242
  log(`❌ ${parsedOptions.error}`, 'red');
242
- log(`\nUso: socc link [--ttl <horas>] [--framework <${ALLOWED_FRAMEWORKS.join('|')}>]\n`, 'yellow');
243
+ log(`\nUsage: socc link [--ttl <hours>] [--framework <${ALLOWED_FRAMEWORKS.join('|')}>]\n`, 'yellow');
243
244
  process.exit(1);
244
245
  }
245
246
 
246
247
  const options = parsedOptions.options;
247
248
 
248
- log('🚀 Socc - Deploy instantáneo\n', 'cyan');
249
+ log('🚀 Socc - Instant deploy\n', 'cyan');
249
250
 
250
251
  const token = getToken();
251
252
 
252
253
  if (!token) {
253
- log('❌ No estás autenticado. Corré `socc login` primero.', 'red');
254
- log('\nPara loguearte: socc login socc_xxxxx\n', 'cyan');
254
+ log('❌ You are not authenticated. Run `socc login` first.', 'red');
255
+ log('\nTo authenticate: socc login socc_xxxxx\n', 'cyan');
255
256
  process.exit(1);
256
257
  }
257
258
 
258
- // Verificar límites
259
- log('🔐 Verificando tu cuenta...', 'yellow');
259
+ // Check limits
260
+ log('🔐 Checking account...', 'yellow');
260
261
  const userInfo = await getUserInfo(token);
261
262
 
262
263
  if (!userInfo.success) {
@@ -266,42 +267,42 @@ async function cmdLink(args) {
266
267
 
267
268
  const { user, usage, canCreateLink } = userInfo;
268
269
 
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');
270
+ log(`✅ Authenticated as: ${user.email}`, 'green');
271
+ log(`📊 Tier: ${user.tier.toUpperCase()}`, 'blue');
272
+ log(`📈 Active deployments: ${usage.activeDeployments}/${usage.maxActiveDeployments}`, 'blue');
273
+ log(`📅 Deployments today: ${usage.linksToday}/${usage.linksLimit}`, 'blue');
273
274
 
274
275
  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');
276
+ log('\n❌ Daily deployment limit reached.', 'red');
277
+ log(`Your ${user.tier} plan allows ${usage.linksLimit} deployments per day.`, 'yellow');
278
+ log('Try again tomorrow or upgrade your plan.\n', 'yellow');
278
279
  process.exit(1);
279
280
  }
280
281
 
281
282
  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');
283
+ log('\n❌ Maximum active deployments reached.', 'red');
284
+ log(`Your ${user.tier} plan allows ${usage.maxActiveDeployments} active deployments.`, 'yellow');
285
+ log('Delete old previews with: socc delete <id>', 'yellow');
286
+ log('Or upgrade your plan.\n', 'yellow');
286
287
  process.exit(1);
287
288
  }
288
289
 
289
- // Detectar framework
290
+ // Detect framework
290
291
  const framework = detectFramework(options.framework);
291
292
  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');
293
+ log(`📦 Framework: ${framework.name}`, 'blue');
294
+ log(`📦 Package manager: ${packageManager}`, 'blue');
295
+ log(`⏰ Requested TTL: ${options.ttl} hour(s)`, 'blue');
295
296
 
296
297
  // Build
297
298
  if (framework.buildScript) {
298
299
  const buildCommand = buildCommandForScript(packageManager, framework.buildScript);
299
- log(`🔨 Construyendo proyecto (${buildCommand})...`, 'yellow');
300
+ log(`🔨 Building project (${buildCommand})...`, 'yellow');
300
301
  try {
301
302
  execSync(buildCommand, { stdio: 'inherit' });
302
- log('✅ Build completado', 'green');
303
+ log('✅ Build completed', 'green');
303
304
  } catch {
304
- log('❌ Build fallido', 'red');
305
+ log('❌ Build failed', 'red');
305
306
  process.exit(1);
306
307
  }
307
308
  }
@@ -314,13 +315,13 @@ async function cmdLink(args) {
314
315
  process.exit(1);
315
316
  }
316
317
 
317
- // Comprimir
318
- log('📦 Comprimiendo archivos...', 'yellow');
318
+ // Compress files
319
+ log('📦 Compressing files...', 'yellow');
319
320
  const zipPath = await zipDirectory(distPath);
320
- log('✅ Archivos comprimidos', 'green');
321
+ log('✅ Files compressed', 'green');
321
322
 
322
323
  // Upload
323
- log('📤 Subiendo a Socc...', 'yellow');
324
+ log('📤 Uploading to Socc...', 'yellow');
324
325
  try {
325
326
  const result = await uploadToApi(zipPath, token, {
326
327
  ttl: options.ttl,
@@ -328,11 +329,11 @@ async function cmdLink(args) {
328
329
  });
329
330
 
330
331
  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');
332
+ log('\n✅ Deploy successful!', 'green');
333
+ log(`\n🚀 Your preview is live at: ${result.url}\n`, 'cyan');
334
+ log(`⏰ Expires in: ${result.ttl} hour(s)\n`, 'blue');
334
335
  } else {
335
- log('❌ Error en el deploy', 'red');
336
+ log('❌ Deployment failed', 'red');
336
337
  if (result.error) {
337
338
  log(result.error, 'red');
338
339
  }
@@ -343,7 +344,7 @@ async function cmdLink(args) {
343
344
  process.exit(1);
344
345
  }
345
346
 
346
- // Limpiar
347
+ // Cleanup
347
348
  try {
348
349
  unlinkSync(zipPath);
349
350
  } catch {
@@ -351,6 +352,23 @@ async function cmdLink(args) {
351
352
  }
352
353
  }
353
354
 
355
+ function cmdLogout() {
356
+ const configPath = join(process.env.HOME, '.socc', 'config.json');
357
+
358
+ if (!existsSync(configPath)) {
359
+ log('ℹ️ No active session found.', 'yellow');
360
+ return;
361
+ }
362
+
363
+ try {
364
+ unlinkSync(configPath);
365
+ log('✅ Logged out successfully.\n', 'green');
366
+ } catch (error) {
367
+ log(`❌ Failed to logout: ${error.message}`, 'red');
368
+ process.exit(1);
369
+ }
370
+ }
371
+
354
372
  // ============= HELPERS =============
355
373
 
356
374
  function getToken() {
@@ -374,13 +392,13 @@ async function getUserInfo(token) {
374
392
  });
375
393
 
376
394
  if (response.status === 401) {
377
- return { success: false, error: 'Token inválido o expirado' };
395
+ return { success: false, error: 'Token is invalid or expired' };
378
396
  }
379
397
 
380
398
  const data = await response.json();
381
399
 
382
400
  if (!data || !data.success) {
383
- return { success: false, error: data?.error || 'Error al verificar usuario' };
401
+ return { success: false, error: data?.error || 'Failed to verify user' };
384
402
  }
385
403
 
386
404
  return {
@@ -391,7 +409,7 @@ async function getUserInfo(token) {
391
409
  canCreateLink: data.canCreateLink !== false
392
410
  };
393
411
  } catch (error) {
394
- return { success: false, error: error.message || 'Error de conexión' };
412
+ return { success: false, error: error.message || 'Connection error' };
395
413
  }
396
414
  }
397
415
 
@@ -405,12 +423,12 @@ function parseLinkOptions(args) {
405
423
  if (arg === '--ttl') {
406
424
  const ttlValue = args[i + 1];
407
425
  if (!ttlValue) {
408
- return { ok: false, error: 'Falta valor para --ttl' };
426
+ return { ok: false, error: 'Missing value for --ttl' };
409
427
  }
410
428
 
411
429
  const parsedTtl = Number.parseInt(ttlValue, 10);
412
430
  if (!Number.isInteger(parsedTtl) || parsedTtl <= 0) {
413
- return { ok: false, error: '--ttl debe ser un entero positivo' };
431
+ return { ok: false, error: '--ttl must be a positive integer' };
414
432
  }
415
433
 
416
434
  ttl = parsedTtl;
@@ -422,7 +440,7 @@ function parseLinkOptions(args) {
422
440
  const ttlValue = arg.split('=')[1];
423
441
  const parsedTtl = Number.parseInt(ttlValue, 10);
424
442
  if (!Number.isInteger(parsedTtl) || parsedTtl <= 0) {
425
- return { ok: false, error: '--ttl debe ser un entero positivo' };
443
+ return { ok: false, error: '--ttl must be a positive integer' };
426
444
  }
427
445
 
428
446
  ttl = parsedTtl;
@@ -432,11 +450,11 @@ function parseLinkOptions(args) {
432
450
  if (arg === '--framework') {
433
451
  const frameworkValue = args[i + 1];
434
452
  if (!frameworkValue) {
435
- return { ok: false, error: 'Falta valor para --framework' };
453
+ return { ok: false, error: 'Missing value for --framework' };
436
454
  }
437
455
 
438
456
  if (!ALLOWED_FRAMEWORKS.includes(frameworkValue)) {
439
- return { ok: false, error: `Framework inválido: ${frameworkValue}` };
457
+ return { ok: false, error: `Invalid framework: ${frameworkValue}` };
440
458
  }
441
459
 
442
460
  framework = frameworkValue;
@@ -447,14 +465,14 @@ function parseLinkOptions(args) {
447
465
  if (arg.startsWith('--framework=')) {
448
466
  const frameworkValue = arg.split('=')[1];
449
467
  if (!ALLOWED_FRAMEWORKS.includes(frameworkValue)) {
450
- return { ok: false, error: `Framework inválido: ${frameworkValue}` };
468
+ return { ok: false, error: `Invalid framework: ${frameworkValue}` };
451
469
  }
452
470
 
453
471
  framework = frameworkValue;
454
472
  continue;
455
473
  }
456
474
 
457
- return { ok: false, error: `Flag no soportada: ${arg}` };
475
+ return { ok: false, error: `Unsupported flag: ${arg}` };
458
476
  }
459
477
 
460
478
  return {
@@ -478,7 +496,7 @@ function detectFramework(preferredFramework = 'auto') {
478
496
  return { ...FRAMEWORK_CONFIGS.static, distCandidates: ['.'] };
479
497
  }
480
498
 
481
- log('❌ No se encontró package.json. Si es un sitio estático, incluí un index.html o usá --framework static.', 'red');
499
+ log('❌ package.json not found. If this is a static site, include index.html or use --framework static.', 'red');
482
500
  process.exit(1);
483
501
  }
484
502
 
@@ -522,7 +540,7 @@ function detectFramework(preferredFramework = 'auto') {
522
540
  return { ...FRAMEWORK_CONFIGS.static, distCandidates: ['.'] };
523
541
  }
524
542
 
525
- log('❌ No se pudo detectar framework ni directorio de salida. Usá --framework o creá script build.', 'red');
543
+ log('❌ Could not detect framework or output directory. Use --framework or add a build script.', 'red');
526
544
  process.exit(1);
527
545
  }
528
546
 
@@ -594,14 +612,14 @@ function resolveDistPath(framework) {
594
612
  }
595
613
 
596
614
  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.');
615
+ throw new Error('Next.js output directory "out" not found. Set `output: "export"` in next.config.js and run the build again.');
598
616
  }
599
617
 
600
618
  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.');
619
+ throw new Error('Build finished, but no output was found in dist/, build/, or out/. Force --framework or adjust your output path.');
602
620
  }
603
621
 
604
- throw new Error(`No se encontró el directorio de salida (${framework.distCandidates.join(', ')})`);
622
+ throw new Error(`Output directory not found (${framework.distCandidates.join(', ')})`);
605
623
  }
606
624
 
607
625
  async function zipDirectory(dirPath) {
@@ -664,6 +682,9 @@ async function main() {
664
682
  case 'login':
665
683
  await cmdLogin(args.slice(1));
666
684
  break;
685
+ case 'logout':
686
+ cmdLogout();
687
+ break;
667
688
  case 'whoami':
668
689
  await cmdWhoami();
669
690
  break;
@@ -677,8 +698,8 @@ async function main() {
677
698
  await cmdDelete(args.slice(1));
678
699
  break;
679
700
  default:
680
- log(`❌ Comando desconocido: ${command}`, 'red');
681
- log('Usá `socc help` para ver los comandos disponibles', 'yellow');
701
+ log(`❌ Unknown command: ${command}`, 'red');
702
+ log('Use `socc help` to see available commands', 'yellow');
682
703
  process.exit(1);
683
704
  }
684
705
  }