kubiy-paas-cli 0.1.21 → 0.1.26

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.
@@ -1,544 +0,0 @@
1
- // src/commands/deploy.js
2
- const fs = require("fs");
3
- const path = require("path");
4
- const os = require("os");
5
- const https = require("https");
6
- const tar = require("tar");
7
- const { apiPost } = require("../api");
8
- const { logInfo, logSuccess, logError } = require("../logger");
9
-
10
- /**
11
- * Helpers para .kubiy.json
12
- */
13
-
14
- function loadProjectKubiyConfig() {
15
- const projectRoot = process.cwd();
16
- const configPath = path.join(projectRoot, ".kubiy.json");
17
-
18
- if (!fs.existsSync(configPath)) {
19
- throw new Error(
20
- "No se encontró .kubiy.json en este proyecto.\n" +
21
- "Ejecuta primero: kubiy link <serviceId>"
22
- );
23
- }
24
-
25
- const raw = fs.readFileSync(configPath, "utf8");
26
- let json;
27
- try {
28
- json = JSON.parse(raw);
29
- } catch (err) {
30
- throw new Error(`.kubiy.json inválido: ${err.message}`);
31
- }
32
-
33
- const { appId, serviceId, environment, apikey } = json;
34
- var healthPath = json.healthPath || "";
35
-
36
- if (!appId || !serviceId) {
37
- throw new Error(
38
- ".kubiy.json incompleto. Debe tener: appId y serviceId.\n" +
39
- JSON.stringify(json, null, 2)
40
- );
41
- }
42
-
43
- return {
44
- appId,
45
- serviceId,
46
- environment,
47
- healthPath,
48
- apikey,
49
- configPath,
50
- config: json // Retornar el objeto completo para poder guardarlo después
51
- };
52
- }
53
-
54
- function saveProjectKubiyConfig(configPath, config) {
55
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
56
- }
57
-
58
- /**
59
- * Leer y parsear archivo .kubiyignore
60
- * Retorna array de patrones a excluir
61
- */
62
- function loadKubiyIgnore(projectRoot) {
63
- const ignorePath = path.join(projectRoot, ".kubiyignore");
64
- const defaultExcludes = [
65
- "node_modules",
66
- ".git",
67
- ".env",
68
- ".DS_Store",
69
- "*.tar",
70
- "*.tar.gz",
71
- "bin",
72
- "obj",
73
- "__pycache__",
74
- "*.pyc",
75
- "vendor",
76
- ];
77
-
78
- if (!fs.existsSync(ignorePath)) {
79
- return defaultExcludes;
80
- }
81
-
82
- try {
83
- const content = fs.readFileSync(ignorePath, "utf8");
84
- const lines = content
85
- .split(/\r?\n/)
86
- .map((line) => line.trim())
87
- .filter((line) => line && !line.startsWith("#")); // Ignorar líneas vacías y comentarios
88
-
89
- // Combinar con exclusiones por defecto y eliminar duplicados
90
- const allExcludes = [...new Set([...defaultExcludes, ...lines])];
91
- return allExcludes;
92
- } catch (err) {
93
- logInfo(`No se pudo leer .kubiyignore: ${err.message}. Usando exclusiones por defecto.`);
94
- return defaultExcludes;
95
- }
96
- }
97
-
98
- /**
99
- * Convertir un patrón estilo glob simple (con `*` y `?`) a RegExp.
100
- * Solo soporta `*` (cualquier secuencia sin `/`) y `?` (un caracter sin `/`).
101
- */
102
- function globToRegex(pattern) {
103
- const escaped = pattern
104
- .replace(/[.+^${}()|[\]\\]/g, "\\$&")
105
- .replace(/\*/g, "[^/]*")
106
- .replace(/\?/g, "[^/]");
107
- return new RegExp(`^${escaped}$`);
108
- }
109
-
110
- /**
111
- * Normalizar patrones de exclusión: quitar ./ inicial y / final.
112
- */
113
- function normalizeExcludePattern(pattern) {
114
- let normalized = pattern.trim();
115
- if (normalized.startsWith("./")) normalized = normalized.substring(2);
116
- if (normalized.endsWith("/")) normalized = normalized.slice(0, -1);
117
- return normalized;
118
- }
119
-
120
- /**
121
- * Construir filtro para la API de `tar` a partir de la lista de patrones.
122
- * Aplica la regla "si cualquier segmento del path coincide, se excluye",
123
- * equivalente al comportamiento de `tar --exclude=` con nombres de carpetas
124
- * o archivos a cualquier profundidad.
125
- */
126
- function buildExcludeFilter(patterns) {
127
- const regexes = patterns
128
- .map(normalizeExcludePattern)
129
- .filter(Boolean)
130
- .map(globToRegex);
131
-
132
- return function shouldInclude(filePath /* , stat */) {
133
- const normalized = filePath.replace(/\\/g, "/").replace(/^\.\//, "");
134
- if (!normalized) return true;
135
- const segments = normalized.split("/").filter(Boolean);
136
- for (const re of regexes) {
137
- if (segments.some((seg) => re.test(seg))) return false;
138
- }
139
- return true;
140
- };
141
- }
142
-
143
- /**
144
- * Empaquetar código en .tar.gz
145
- * - Se genera en el directorio temporal del sistema (NO dentro del repo)
146
- * - Usa .kubiyignore para excluir archivos y carpetas
147
- * - No depende de ningún binario del SO: usa el paquete `tar` de npm
148
- */
149
- async function createTarball(deployType) {
150
- const projectRoot = process.cwd();
151
- // Si deployType es "server", usar nombre fijo deploy.tar.gz, sino usar timestamp
152
- const archiveName = deployType === "server" ? "deploy.tar.gz" : `deploy-${Date.now()}.tar.gz`;
153
- const archivePath = path.join(os.tmpdir(), archiveName);
154
-
155
- logInfo("Empaquetando código en " + archiveName + " ...");
156
-
157
- const excludes = loadKubiyIgnore(projectRoot);
158
- const filter = buildExcludeFilter(excludes);
159
-
160
- try {
161
- await tar.c(
162
- {
163
- gzip: true,
164
- file: archivePath,
165
- cwd: projectRoot,
166
- portable: true,
167
- filter,
168
- },
169
- ["."]
170
- );
171
- } catch (err) {
172
- throw new Error("Error al crear el .tar.gz: " + (err.message || String(err)));
173
- }
174
-
175
- logSuccess("Artefacto generado en temp: " + archivePath);
176
- return archivePath;
177
- }
178
-
179
- /**
180
- * Empaquetar directorio static en .tar.gz
181
- * - Se genera en el directorio temporal del sistema
182
- * - Empaqueta el contenido del directorio directamente (sin la carpeta contenedora)
183
- * - No depende de ningún binario del SO: usa el paquete `tar` de npm
184
- */
185
- async function createStaticTarball(staticDir) {
186
- const archiveName = `static-deploy-${Date.now()}.tar.gz`;
187
- const archivePath = path.join(os.tmpdir(), archiveName);
188
-
189
- logInfo(`Empaquetando directorio static: ${staticDir} en ${archiveName}...`);
190
-
191
- try {
192
- await tar.c(
193
- {
194
- gzip: true,
195
- file: archivePath,
196
- cwd: staticDir,
197
- portable: true,
198
- },
199
- ["."]
200
- );
201
- } catch (err) {
202
- throw new Error(
203
- "Error al crear el .tar.gz del directorio static: " + (err.message || String(err))
204
- );
205
- }
206
-
207
- logSuccess("Artefacto static generado en temp: " + archivePath);
208
- return archivePath;
209
- }
210
-
211
- /**
212
- * Subir marker JSON a S3 con presigned URL (PUT)
213
- */
214
- function uploadMarker(markerUploadUrl, deployId, releaseId) {
215
- return new Promise((resolve, reject) => {
216
- logInfo('markerUploadUrl: ' + markerUploadUrl);
217
- const url = new URL(markerUploadUrl);
218
- logInfo('url: ' + JSON.stringify(url));
219
- const marker = {
220
- deployId,
221
- releaseId,
222
- createdAt: new Date().toISOString(),
223
- };
224
-
225
- const payload = JSON.stringify(marker);
226
- const options = {
227
- method: "PUT",
228
- hostname: url.hostname,
229
- port: url.port || 443,
230
- path: url.pathname + url.search,
231
- headers: {
232
- "Content-Type": "application/json",
233
- "Content-Length": Buffer.byteLength(payload),
234
- },
235
- };
236
- logInfo('options: ' + JSON.stringify(options));
237
-
238
- const req = https.request(options, (res) => {
239
- if (res.statusCode >= 200 && res.statusCode < 300) {
240
- resolve();
241
- } else {
242
- let data = "";
243
- res.on("data", (chunk) => (data += chunk));
244
- res.on("end", () => {
245
- reject(
246
- new Error(
247
- `Marker upload failed: HTTP ${res.statusCode} ${res.statusMessage} - ${data}`
248
- )
249
- );
250
- });
251
- }
252
- });
253
-
254
- req.on("error", reject);
255
- req.write(payload);
256
- req.end();
257
- });
258
- }
259
-
260
- /**
261
- * Subir archivo a S3 con presigned URL (PUT)
262
- */
263
- function uploadFileWithPresignedUrl(urlString, filePath) {
264
- return new Promise((resolve, reject) => {
265
- const url = new URL(urlString);
266
-
267
- // Leer tamaño del archivo para evitar Transfer-Encoding: chunked
268
- let stat;
269
- try {
270
- stat = fs.statSync(filePath);
271
- } catch (err) {
272
- return reject(new Error(`No se pudo leer el archivo a subir: ${err.message}`));
273
- }
274
-
275
- const fileStream = fs.createReadStream(filePath);
276
- fileStream.on("error", reject);
277
-
278
- const options = {
279
- method: "PUT",
280
- hostname: url.hostname,
281
- port: url.port || 443,
282
- path: url.pathname + url.search,
283
- headers: {
284
- "Content-Type": "application/octet-stream",
285
- "Content-Length": stat.size,
286
- },
287
- };
288
-
289
- const req = https.request(options, (res) => {
290
- if (res.statusCode >= 200 && res.statusCode < 300) {
291
- resolve();
292
- } else {
293
- let data = "";
294
- res.on("data", (chunk) => (data += chunk));
295
- res.on("end", () => {
296
- reject(
297
- new Error(
298
- `Error al subir a S3: HTTP ${res.statusCode} ${res.statusMessage} - ${data}`
299
- )
300
- );
301
- });
302
- }
303
- });
304
-
305
- req.on("error", reject);
306
-
307
- fileStream.pipe(req);
308
- });
309
- }
310
-
311
- /**
312
- * Seleccionar directorio de salida para deploy static
313
- */
314
- function pickStaticOutDir(projectDir, overrideDir) {
315
- if (overrideDir) {
316
- const p = path.resolve(projectDir, overrideDir);
317
- if (!fs.existsSync(p)) {
318
- throw new Error(`Output dir not found: ${overrideDir}`);
319
- }
320
- return p;
321
- }
322
-
323
- const candidates = ["dist", "build", "out", path.join(".output", "public")];
324
- for (const c of candidates) {
325
- const p = path.resolve(projectDir, c);
326
- if (fs.existsSync(p) && fs.statSync(p).isDirectory()) {
327
- return p;
328
- }
329
- }
330
-
331
- throw new Error(
332
- `No static output folder found. Expected one of: dist/, build/, out/, .output/public/. ` +
333
- `Run your build first or specify --dir <folder>.`
334
- );
335
- }
336
-
337
- /**
338
- * Verificar que el build static tenga index.html
339
- */
340
- function assertStaticBuild(outDir) {
341
- const indexPath = path.join(outDir, "index.html");
342
- if (!fs.existsSync(indexPath)) {
343
- throw new Error(`index.html not found in ${outDir}. Did you run the build?`);
344
- }
345
- }
346
-
347
- /**
348
- * Parsear argumentos del comando deploy
349
- */
350
- function parseArgs(args) {
351
- const opts = {
352
- static: false,
353
- dir: null,
354
- message: null,
355
- };
356
-
357
- for (let i = 0; i < args.length; i++) {
358
- const a = args[i];
359
- if (a === "--static") {
360
- opts.static = true;
361
- } else if (a === "--dir" && i + 1 < args.length) {
362
- opts.dir = args[i + 1];
363
- i++;
364
- } else if (a.startsWith("--dir=")) {
365
- opts.dir = a.split("=")[1];
366
- } else if (a === "--message" && i + 1 < args.length) {
367
- opts.message = args[i + 1];
368
- i++;
369
- } else if (a.startsWith("--message=")) {
370
- opts.message = a.slice("--message=".length);
371
- } else if ((a === "-m") && i + 1 < args.length) {
372
- opts.message = args[i + 1];
373
- i++;
374
- }
375
- }
376
-
377
- return opts;
378
- }
379
-
380
- /**
381
- * Comando principal: kubiy deploy
382
- * Empaqueta el código y lo sube a S3
383
- */
384
-
385
- async function run(args = []) {
386
- let archivePath = null;
387
-
388
- try {
389
- const opts = parseArgs(args);
390
-
391
- // 3) Cargar config del proyecto (.kubiy.json)
392
- const kubiyConfig = loadProjectKubiyConfig();
393
- const { appId, serviceId, environment, healthPath, apikey, configPath, config } = kubiyConfig;
394
-
395
- // La autenticación se hace con el apikey de .kubiy.json (generado por `kubiy link`),
396
- // no con el CLI token. Si falta el apikey, hay que re-linkear el proyecto.
397
- if (!apikey) {
398
- logError("No se encontró 'apikey' en .kubiy.json. Vuelve a linkear el proyecto:");
399
- logError(" kubiy link <appId> --apikey=<tu-api-key>");
400
- process.exit(1);
401
- }
402
-
403
- // Validar que deployType coincida con el modo de deploy
404
- const deployType = config.deployType;
405
- if (!opts.static && deployType === "static") {
406
- logError("Error: El proyecto está configurado como 'static' en .kubiy.json (deployType: static).");
407
- logError("Para hacer deploy de este proyecto, usa: kubiy deploy --static");
408
- process.exit(1);
409
- }
410
- if (opts.static && deployType && deployType !== "static") {
411
- logError(`Error: El proyecto está configurado con deployType: '${deployType}' en .kubiy.json, pero estás intentando hacer deploy static.`);
412
- logError("Para hacer deploy de este proyecto, usa: kubiy deploy (sin --static)");
413
- process.exit(1);
414
- }
415
-
416
- // Mensaje obligatorio que identifica el deploy (se envía como commit)
417
- const commit = (opts.message && opts.message.trim()) ? opts.message.trim() : null;
418
- if (!commit) {
419
- logError("Debes indicar un mensaje que identifique este deploy.");
420
- logError("Uso: kubiy deploy --message \"Descripción del deploy\"");
421
- logError(" o: kubiy deploy -m \"Descripción del deploy\"");
422
- logError(" o: kubiy deploy --message=Descripción");
423
- process.exit(1);
424
- }
425
-
426
- // 4) Crear tarball (en temp)
427
- if (opts.static) {
428
- const projectRoot = process.cwd();
429
-
430
- // Prioridad: 1) --dir del CLI, 2) static.outDir del config, 3) autodetección
431
- const cliDir = opts.dir;
432
- const configOutDir = config.static?.outDir || null;
433
-
434
- const outDir = pickStaticOutDir(projectRoot, cliDir || configOutDir);
435
-
436
- // Si se detectó automáticamente y no está en el config, guardarlo
437
- if (!configOutDir && !cliDir) {
438
- config.static = config.static || {};
439
- const relativeOutDir = path.relative(projectRoot, outDir).replace(/\\/g, "/");
440
- config.static.outDir = relativeOutDir;
441
- saveProjectKubiyConfig(configPath, config);
442
- logInfo(`Detected output dir: ${config.static.outDir} (saved)`);
443
- }
444
-
445
- logInfo(`Usando directorio static: ${outDir}`);
446
- assertStaticBuild(outDir);
447
- archivePath = await createStaticTarball(outDir);
448
- } else {
449
- archivePath = await createTarball(deployType);
450
- }
451
-
452
- // 5) Pedir presigned URL a la API Kubiy
453
- logInfo("Solicitando presigned URL a Kubiy...");
454
-
455
- const presignBody = {
456
- appId,
457
- serviceId,
458
- commit,
459
- env: environment?.name,
460
- healthPath: healthPath
461
- };
462
-
463
- // Si es static, agregar el parámetro static al body
464
- if (opts.static) {
465
- presignBody.static = true;
466
- }
467
-
468
- const presignResp = await apiPost("/deploys/presign", presignBody, {
469
- headers: {
470
- "x-kubiy-token": apikey
471
- }
472
- });
473
-
474
- //logInfo(JSON.stringify(presignResp, null, 2));
475
- const { deployId, releaseId } = presignResp || {};
476
- if (!deployId || !releaseId) {
477
- throw new Error(
478
- "Respuesta de /deploys/presign incompleta. Falta deployId o releaseId.\n" +
479
- JSON.stringify(presignResp, null, 2)
480
- );
481
- }
482
-
483
- let key = presignResp?.key;
484
- let uploadUrl = presignResp?.uploadUrl;
485
- if (!key || !uploadUrl) {
486
- throw new Error(
487
- "Respuesta de /deploys/presign incompleta. Falta key o uploadUrl.\n" +
488
- JSON.stringify(presignResp, null, 2)
489
- );
490
- }
491
-
492
- logSuccess(`Presign OK. deployId=${deployId} releaseId=${releaseId}`);
493
-
494
- // 6) Subir a S3 usando uploadUrl si no es static
495
- logInfo("Subiendo artefacto a S3...");
496
- await uploadFileWithPresignedUrl(uploadUrl, archivePath);
497
- logSuccess("Artefacto subido correctamente a S3.");
498
-
499
- // 7) Avisar a Kubiy que inicie el deploy (POST /deploys)
500
- // En deploys static no se llama a /deploys: la subida del artefacto al
501
- // bucket presignado es suficiente para que el backend publique el sitio.
502
- if (opts.static) {
503
- logSuccess("🚀 Deploy static publicado correctamente en Kubiy.");
504
- logInfo(`deployId=${deployId} releaseId=${releaseId}`);
505
- } else {
506
- logInfo("Notificando a Kubiy que inicie el deploy...");
507
-
508
- const deployResp = await apiPost("/deploys", {
509
- deployId,
510
- appId,
511
- serviceId,
512
- artifactKey: key,
513
- commit,
514
- env: environment?.name
515
- }, {
516
- headers: {
517
- "x-kubiy-token": apikey
518
- }
519
- });
520
-
521
- logSuccess("🚀 Deploy iniciado correctamente en Kubiy.");
522
- console.log(JSON.stringify(deployResp, null, 2));
523
- }
524
-
525
- logInfo(`Puedes consultar el servicio con: kubiy service:get ${serviceId}`);
526
- logInfo(`Y los logs con: kubiy logs ${serviceId} --kind=deploy --lines=200`);
527
- } catch (err) {
528
- logError("Error en kubiy deploy:");
529
- logError(err.message || err);
530
- process.exit(1);
531
- } finally {
532
- // 8) Limpiar el artefacto local en temp
533
- if (archivePath && fs.existsSync(archivePath)) {
534
- try {
535
- fs.unlinkSync(archivePath);
536
- logInfo(`Artefacto temporal eliminado: ${archivePath}`);
537
- } catch (e) {
538
- logError(`No se pudo eliminar artefacto temporal: ${archivePath} (${e.message})`);
539
- }
540
- }
541
- }
542
- }
543
-
544
- module.exports = { run };
@@ -1,16 +0,0 @@
1
- const { setCliToken } = require("../config");
2
- const { logSuccess, logError } = require("../logger");
3
-
4
- async function run(args) {
5
- const token = args[0];
6
-
7
- if (!token) {
8
- logError("Uso: kubiy login <cliToken>");
9
- process.exit(1);
10
- }
11
-
12
- setCliToken(token.trim());
13
- logSuccess("CLI token guardado en ~/.kubiy/config.json");
14
- }
15
-
16
- module.exports = { run };
@@ -1,112 +0,0 @@
1
- // src/commands/logs.js
2
- const fs = require("fs");
3
- const path = require("path");
4
- const { apiGet } = require("../api");
5
- const { logInfo, logError } = require("../logger");
6
-
7
- function parseArgs(args) {
8
- const opts = {
9
- kind: "app",
10
- lines: 200,
11
- };
12
- const clean = [];
13
-
14
- for (const a of args) {
15
- if (a.startsWith("--kind=")) {
16
- opts.kind = a.split("=")[1] || "app";
17
- } else if (a.startsWith("--lines=")) {
18
- const v = parseInt(a.split("=")[1] || "200", 10);
19
- if (!isNaN(v) && v > 0) opts.lines = v;
20
- } else {
21
- clean.push(a);
22
- }
23
- }
24
-
25
- // clean[0] puede o no tener serviceId
26
- return { serviceId: clean[0] || null, opts };
27
- }
28
-
29
- function loadKubiyLink() {
30
- const filepath = path.join(process.cwd(), ".kubiy.json");
31
-
32
- if (!fs.existsSync(filepath)) {
33
- return null;
34
- }
35
-
36
- try {
37
- const raw = fs.readFileSync(filepath, "utf8");
38
- const cfg = JSON.parse(raw);
39
-
40
- return { serviceId: cfg.serviceId || null, apikey: cfg.apikey || null };
41
- } catch (err) {
42
- // Si el json está roto, que el usuario se entere
43
- logError("No se pudo leer .kubiy.json:", err.message || String(err));
44
- return null;
45
- }
46
- }
47
-
48
- async function run(args) {
49
- const { serviceId: cliServiceId, opts } = parseArgs(args);
50
-
51
- const linked = loadKubiyLink();
52
- let serviceId = cliServiceId;
53
- const apikey = linked && linked.apikey;
54
-
55
- // 🍀 Si no viene serviceId por CLI, intentamos usar el del link
56
- if (!serviceId) {
57
- const linkedServiceId = linked && linked.serviceId;
58
- if (!linkedServiceId) {
59
- logError(
60
- "Uso: kubiy logs <serviceId> --kind=deploy|app --lines=200\n" +
61
- "O bien, ejecuta primero: kubiy link <serviceId> y luego solo 'kubiy logs'."
62
- );
63
- process.exit(1);
64
- }
65
- serviceId = linkedServiceId;
66
- logInfo(`Usando serviceId del link (.kubiy.json): ${serviceId}`);
67
- }
68
-
69
- if (!apikey) {
70
- logError("No se encontró 'apikey' en .kubiy.json. Vuelve a linkear el proyecto:");
71
- logError(" kubiy link <appId> --apikey=<tu-api-key>");
72
- process.exit(1);
73
- }
74
-
75
- const kind = (opts.kind || "app").toLowerCase();
76
- const lines = opts.lines || 200;
77
-
78
- try {
79
- logInfo(
80
- `Obteniendo logs (${kind}) del servicio ${serviceId} (últimas ${lines} líneas)...`
81
- );
82
-
83
- const qs = new URLSearchParams({
84
- kind,
85
- lines: String(lines),
86
- }).toString();
87
-
88
- const pathUrl = `/services/${encodeURIComponent(serviceId)}/logs?${qs}`;
89
-
90
- const resp = await apiGet(pathUrl, {
91
- headers: { "x-kubiy-token": apikey },
92
- }); // { stdout, stderr, ... }
93
- const stdout = resp.stdout || "";
94
- const stderr = resp.stderr || "";
95
-
96
- if (stdout) {
97
- process.stdout.write(stdout.endsWith("\n") ? stdout : stdout + "\n");
98
- }
99
-
100
- // Si quieres mostrar stderr como comentario abajo:
101
- if (stderr) {
102
- console.error("\n[STDERR]");
103
- process.stderr.write(stderr.endsWith("\n") ? stderr : stderr + "\n");
104
- }
105
- } catch (err) {
106
- logError("Error al obtener logs:");
107
- logError(err.message || String(err));
108
- process.exit(1);
109
- }
110
- }
111
-
112
- module.exports = { run };