gufi-cli 0.1.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.
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Gufi Dev CLI - Sync Library
3
+ * Handles bidirectional file synchronization
4
+ */
5
+ export interface ViewMeta {
6
+ viewId: number;
7
+ viewName: string;
8
+ packageId: number;
9
+ lastSync: string;
10
+ files: Record<string, {
11
+ hash: string;
12
+ mtime: number;
13
+ }>;
14
+ }
15
+ export declare function getViewDir(viewName: string): string;
16
+ export declare function loadViewMeta(viewDir: string): ViewMeta | null;
17
+ /**
18
+ * Pull view files from Gufi to local directory
19
+ */
20
+ export declare function pullView(viewId: number, viewName: string, packageId: number): Promise<{
21
+ dir: string;
22
+ fileCount: number;
23
+ }>;
24
+ /**
25
+ * Push local files to Gufi
26
+ */
27
+ export declare function pushView(viewDir?: string): Promise<{
28
+ pushed: number;
29
+ }>;
30
+ /**
31
+ * Check for local changes that need pushing
32
+ */
33
+ export declare function getChangedFiles(viewDir: string): string[];
34
+ /**
35
+ * Push a single file
36
+ */
37
+ export declare function pushSingleFile(viewDir: string, relativePath: string): Promise<void>;
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Gufi Dev CLI - Sync Library
3
+ * Handles bidirectional file synchronization
4
+ */
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import os from "os";
8
+ import { getViewFiles, saveViewFile } from "./api.js";
9
+ import { setCurrentView, getCurrentView } from "./config.js";
10
+ const GUFI_DEV_DIR = path.join(os.homedir(), "gufi-dev");
11
+ const META_FILE = ".gufi-view.json";
12
+ function ensureDir(dir) {
13
+ if (!fs.existsSync(dir)) {
14
+ fs.mkdirSync(dir, { recursive: true });
15
+ }
16
+ }
17
+ function hashContent(content) {
18
+ // Simple hash for change detection
19
+ let hash = 0;
20
+ for (let i = 0; i < content.length; i++) {
21
+ const char = content.charCodeAt(i);
22
+ hash = ((hash << 5) - hash) + char;
23
+ hash = hash & hash;
24
+ }
25
+ return hash.toString(16);
26
+ }
27
+ function getLanguage(filePath) {
28
+ const ext = path.extname(filePath).toLowerCase();
29
+ const langMap = {
30
+ ".ts": "typescript",
31
+ ".tsx": "typescript",
32
+ ".js": "javascript",
33
+ ".jsx": "javascript",
34
+ ".css": "css",
35
+ ".json": "json",
36
+ ".md": "markdown",
37
+ };
38
+ return langMap[ext] || "text";
39
+ }
40
+ export function getViewDir(viewName) {
41
+ return path.join(GUFI_DEV_DIR, viewName.toLowerCase().replace(/\s+/g, "-"));
42
+ }
43
+ export function loadViewMeta(viewDir) {
44
+ const metaPath = path.join(viewDir, META_FILE);
45
+ if (!fs.existsSync(metaPath))
46
+ return null;
47
+ try {
48
+ return JSON.parse(fs.readFileSync(metaPath, "utf-8"));
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ function saveViewMeta(viewDir, meta) {
55
+ const metaPath = path.join(viewDir, META_FILE);
56
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
57
+ }
58
+ /**
59
+ * Pull view files from Gufi to local directory
60
+ */
61
+ export async function pullView(viewId, viewName, packageId) {
62
+ const viewDir = getViewDir(viewName);
63
+ ensureDir(viewDir);
64
+ const files = await getViewFiles(viewId);
65
+ const fileMeta = {};
66
+ for (const file of files) {
67
+ const filePath = path.join(viewDir, file.file_path.replace(/^\//, ""));
68
+ ensureDir(path.dirname(filePath));
69
+ fs.writeFileSync(filePath, file.content);
70
+ fileMeta[file.file_path] = {
71
+ hash: hashContent(file.content),
72
+ mtime: Date.now(),
73
+ };
74
+ }
75
+ const meta = {
76
+ viewId,
77
+ viewName,
78
+ packageId,
79
+ lastSync: new Date().toISOString(),
80
+ files: fileMeta,
81
+ };
82
+ saveViewMeta(viewDir, meta);
83
+ // Update current view in config
84
+ setCurrentView({ id: viewId, name: viewName, packageId, localPath: viewDir });
85
+ return { dir: viewDir, fileCount: files.length };
86
+ }
87
+ /**
88
+ * Push local files to Gufi
89
+ */
90
+ export async function pushView(viewDir) {
91
+ const dir = viewDir || getCurrentView()?.localPath;
92
+ if (!dir) {
93
+ throw new Error("No hay vista activa. Usa: gufi pull <vista>");
94
+ }
95
+ const meta = loadViewMeta(dir);
96
+ if (!meta) {
97
+ throw new Error("No se encontró metadata de vista. ¿Hiciste pull primero?");
98
+ }
99
+ const localFiles = getLocalFiles(dir);
100
+ let pushed = 0;
101
+ for (const file of localFiles) {
102
+ const content = fs.readFileSync(path.join(dir, file), "utf-8");
103
+ const hash = hashContent(content);
104
+ const filePath = "/" + file;
105
+ // Check if file changed
106
+ const oldMeta = meta.files[filePath];
107
+ if (oldMeta && oldMeta.hash === hash) {
108
+ continue; // No changes
109
+ }
110
+ // Push to Gufi
111
+ await saveViewFile(meta.viewId, {
112
+ file_path: filePath,
113
+ content,
114
+ language: getLanguage(file),
115
+ is_entry_point: file === "index.tsx",
116
+ });
117
+ // Update meta
118
+ meta.files[filePath] = { hash, mtime: Date.now() };
119
+ pushed++;
120
+ }
121
+ meta.lastSync = new Date().toISOString();
122
+ saveViewMeta(dir, meta);
123
+ return { pushed };
124
+ }
125
+ /**
126
+ * Get list of local files (excluding meta and hidden)
127
+ */
128
+ function getLocalFiles(dir, prefix = "") {
129
+ const files = [];
130
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
131
+ for (const entry of entries) {
132
+ if (entry.name.startsWith("."))
133
+ continue; // Skip hidden files
134
+ const fullPath = path.join(prefix, entry.name);
135
+ if (entry.isDirectory()) {
136
+ files.push(...getLocalFiles(path.join(dir, entry.name), fullPath));
137
+ }
138
+ else {
139
+ files.push(fullPath);
140
+ }
141
+ }
142
+ return files;
143
+ }
144
+ /**
145
+ * Check for local changes that need pushing
146
+ */
147
+ export function getChangedFiles(viewDir) {
148
+ const meta = loadViewMeta(viewDir);
149
+ if (!meta)
150
+ return [];
151
+ const changed = [];
152
+ const localFiles = getLocalFiles(viewDir);
153
+ for (const file of localFiles) {
154
+ const content = fs.readFileSync(path.join(viewDir, file), "utf-8");
155
+ const hash = hashContent(content);
156
+ const filePath = "/" + file;
157
+ const oldMeta = meta.files[filePath];
158
+ if (!oldMeta || oldMeta.hash !== hash) {
159
+ changed.push(file);
160
+ }
161
+ }
162
+ return changed;
163
+ }
164
+ /**
165
+ * Push a single file
166
+ */
167
+ export async function pushSingleFile(viewDir, relativePath) {
168
+ const meta = loadViewMeta(viewDir);
169
+ if (!meta) {
170
+ throw new Error("No se encontró metadata de vista");
171
+ }
172
+ const fullPath = path.join(viewDir, relativePath);
173
+ const content = fs.readFileSync(fullPath, "utf-8");
174
+ const filePath = "/" + relativePath.replace(/\\/g, "/");
175
+ await saveViewFile(meta.viewId, {
176
+ file_path: filePath,
177
+ content,
178
+ language: getLanguage(relativePath),
179
+ is_entry_point: relativePath === "index.tsx",
180
+ });
181
+ // Update meta
182
+ meta.files[filePath] = {
183
+ hash: hashContent(content),
184
+ mtime: Date.now(),
185
+ };
186
+ meta.lastSync = new Date().toISOString();
187
+ saveViewMeta(viewDir, meta);
188
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "gufi-cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI for developing Gufi Marketplace views locally with Claude Code",
5
+ "bin": {
6
+ "gufi": "./bin/gufi.js"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/juanbp23/gogufi"
11
+ },
12
+ "homepage": "https://gogufi.com",
13
+ "type": "module",
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "dev": "tsc -w",
17
+ "start": "node bin/gufi.js"
18
+ },
19
+ "dependencies": {
20
+ "chalk": "^5.3.0",
21
+ "chokidar": "^3.5.3",
22
+ "commander": "^12.0.0",
23
+ "node-fetch": "^3.3.2",
24
+ "ora": "^8.0.1"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^20.10.0",
28
+ "typescript": "^5.3.0"
29
+ },
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "keywords": ["gufi", "developer", "cli", "marketplace"],
34
+ "author": "Gufi",
35
+ "license": "MIT"
36
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * gufi login - Authenticate with Gufi
3
+ */
4
+
5
+ import readline from "readline";
6
+ import chalk from "chalk";
7
+ import ora from "ora";
8
+ import { login, validateToken } from "../lib/api.js";
9
+ import { setToken, isLoggedIn, clearToken, loadConfig, setApiUrl } from "../lib/config.js";
10
+
11
+ function prompt(question: string, hidden = false): Promise<string> {
12
+ const rl = readline.createInterface({
13
+ input: process.stdin,
14
+ output: process.stdout,
15
+ });
16
+
17
+ return new Promise((resolve) => {
18
+ if (hidden) {
19
+ process.stdout.write(question);
20
+ let input = "";
21
+ process.stdin.setRawMode?.(true);
22
+ process.stdin.resume();
23
+ process.stdin.on("data", (char) => {
24
+ const c = char.toString();
25
+ if (c === "\n" || c === "\r") {
26
+ process.stdin.setRawMode?.(false);
27
+ process.stdout.write("\n");
28
+ rl.close();
29
+ resolve(input);
30
+ } else if (c === "\u0003") {
31
+ process.exit();
32
+ } else if (c === "\u007F") {
33
+ if (input.length > 0) {
34
+ input = input.slice(0, -1);
35
+ process.stdout.write("\b \b");
36
+ }
37
+ } else {
38
+ input += c;
39
+ process.stdout.write("*");
40
+ }
41
+ });
42
+ } else {
43
+ rl.question(question, (answer) => {
44
+ rl.close();
45
+ resolve(answer);
46
+ });
47
+ }
48
+ });
49
+ }
50
+
51
+ export async function loginCommand(options: { api?: string }): Promise<void> {
52
+ console.log(chalk.magenta("\n 🟣 Gufi Developer CLI\n"));
53
+
54
+ // Set custom API URL if provided
55
+ if (options.api) {
56
+ setApiUrl(options.api);
57
+ console.log(chalk.gray(` API: ${options.api}\n`));
58
+ }
59
+
60
+ // Check if already logged in
61
+ if (isLoggedIn()) {
62
+ const config = loadConfig();
63
+ const spinner = ora("Verificando sesión...").start();
64
+ const valid = await validateToken();
65
+
66
+ if (valid) {
67
+ spinner.succeed(chalk.green(`Ya estás logueado como ${config.email}`));
68
+ const relogin = await prompt("\n¿Quieres iniciar sesión con otra cuenta? (s/N): ");
69
+ if (relogin.toLowerCase() !== "s") {
70
+ return;
71
+ }
72
+ clearToken();
73
+ } else {
74
+ spinner.warn("Sesión expirada");
75
+ clearToken();
76
+ }
77
+ }
78
+
79
+ // Get credentials
80
+ const email = await prompt(" Email: ");
81
+ const password = await prompt(" Password: ", true);
82
+
83
+ if (!email || !password) {
84
+ console.log(chalk.red("\n ✗ Email y password son requeridos\n"));
85
+ process.exit(1);
86
+ }
87
+
88
+ const spinner = ora("Iniciando sesión...").start();
89
+
90
+ try {
91
+ const { token } = await login(email, password);
92
+ setToken(token, email);
93
+ spinner.succeed(chalk.green(`Sesión iniciada como ${email}`));
94
+ console.log(chalk.gray("\n Ahora puedes usar: gufi pull <vista>\n"));
95
+ } catch (error: any) {
96
+ spinner.fail(chalk.red(error.message || "Error al iniciar sesión"));
97
+ process.exit(1);
98
+ }
99
+ }
100
+
101
+ export async function logoutCommand(): Promise<void> {
102
+ clearToken();
103
+ console.log(chalk.green("\n ✓ Sesión cerrada\n"));
104
+ }
105
+
106
+ export async function whoamiCommand(): Promise<void> {
107
+ const config = loadConfig();
108
+
109
+ if (!isLoggedIn()) {
110
+ console.log(chalk.yellow("\n No estás logueado. Usa: gufi login\n"));
111
+ return;
112
+ }
113
+
114
+ const spinner = ora("Verificando...").start();
115
+ const valid = await validateToken();
116
+
117
+ if (valid) {
118
+ spinner.succeed(chalk.green(`Logueado como ${config.email}`));
119
+ console.log(chalk.gray(` API: ${config.apiUrl}\n`));
120
+ } else {
121
+ spinner.fail(chalk.red("Sesión expirada"));
122
+ console.log(chalk.gray(" Usa: gufi login\n"));
123
+ }
124
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * gufi pull - Download view files from Gufi
3
+ */
4
+
5
+ import chalk from "chalk";
6
+ import ora from "ora";
7
+ import { listPackages, listViews, getView } from "../lib/api.js";
8
+ import { pullView } from "../lib/sync.js";
9
+ import { isLoggedIn } from "../lib/config.js";
10
+
11
+ export async function pullCommand(viewIdentifier?: string): Promise<void> {
12
+ if (!isLoggedIn()) {
13
+ console.log(chalk.red("\n ✗ No estás logueado. Usa: gufi login\n"));
14
+ process.exit(1);
15
+ }
16
+
17
+ console.log(chalk.magenta("\n 🟣 Gufi Pull\n"));
18
+
19
+ let viewId: number;
20
+ let viewName: string;
21
+ let packageId: number;
22
+
23
+ // If view ID provided directly
24
+ if (viewIdentifier && /^\d+$/.test(viewIdentifier)) {
25
+ viewId = parseInt(viewIdentifier);
26
+ const spinner = ora("Obteniendo vista...").start();
27
+ try {
28
+ const view = await getView(viewId);
29
+ viewName = view.name;
30
+ packageId = view.package_id;
31
+ spinner.succeed(`Vista: ${viewName}`);
32
+ } catch (error: any) {
33
+ spinner.fail(chalk.red(`No se encontró la vista ${viewId}`));
34
+ process.exit(1);
35
+ }
36
+ } else {
37
+ // Interactive selection
38
+ const spinner = ora("Cargando packages...").start();
39
+
40
+ try {
41
+ const packages = await listPackages();
42
+ spinner.stop();
43
+
44
+ if (packages.length === 0) {
45
+ console.log(chalk.yellow(" No tienes packages. Crea uno en Developer Center.\n"));
46
+ process.exit(0);
47
+ }
48
+
49
+ console.log(chalk.gray(" Tus packages:\n"));
50
+ packages.forEach((pkg, i) => {
51
+ console.log(` ${chalk.cyan(i + 1)}. ${pkg.name}`);
52
+ });
53
+
54
+ // For now, use first package (can improve with interactive selection later)
55
+ const pkg = packages[0];
56
+ packageId = pkg.pk_id;
57
+ console.log(chalk.gray(`\n Usando package: ${pkg.name}\n`));
58
+
59
+ const viewsSpinner = ora("Cargando vistas...").start();
60
+ const views = await listViews(packageId);
61
+ viewsSpinner.stop();
62
+
63
+ if (views.length === 0) {
64
+ console.log(chalk.yellow(" No hay vistas en este package.\n"));
65
+ process.exit(0);
66
+ }
67
+
68
+ console.log(chalk.gray(" Vistas disponibles:\n"));
69
+ views.forEach((view, i) => {
70
+ console.log(` ${chalk.cyan(i + 1)}. ${view.name} ${chalk.gray(`(${view.view_type})`)}`);
71
+ });
72
+
73
+ // If viewIdentifier matches a name
74
+ let selectedView = viewIdentifier
75
+ ? views.find(v => v.name.toLowerCase().includes(viewIdentifier.toLowerCase()))
76
+ : views[0];
77
+
78
+ if (!selectedView) {
79
+ console.log(chalk.yellow(`\n No se encontró vista "${viewIdentifier}"\n`));
80
+ console.log(chalk.gray(" Uso: gufi pull <nombre-vista> o gufi pull <view-id>\n"));
81
+ process.exit(1);
82
+ }
83
+
84
+ viewId = selectedView.pk_id;
85
+ viewName = selectedView.name;
86
+
87
+ console.log(chalk.gray(`\n Descargando: ${viewName}\n`));
88
+
89
+ } catch (error: any) {
90
+ spinner.fail(chalk.red(error.message));
91
+ process.exit(1);
92
+ }
93
+ }
94
+
95
+ // Pull the view
96
+ const pullSpinner = ora("Descargando archivos...").start();
97
+
98
+ try {
99
+ const result = await pullView(viewId, viewName, packageId);
100
+ pullSpinner.succeed(chalk.green(`${result.fileCount} archivos descargados`));
101
+
102
+ console.log(chalk.gray(`\n 📁 ${result.dir}\n`));
103
+ console.log(chalk.gray(" Comandos útiles:"));
104
+ console.log(chalk.cyan(" cd " + result.dir));
105
+ console.log(chalk.cyan(" gufi watch") + chalk.gray(" # Auto-sync cambios"));
106
+ console.log(chalk.cyan(" claude") + chalk.gray(" # Abrir Claude Code"));
107
+ console.log();
108
+
109
+ } catch (error: any) {
110
+ pullSpinner.fail(chalk.red(error.message));
111
+ process.exit(1);
112
+ }
113
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * gufi push - Upload local changes to Gufi
3
+ */
4
+
5
+ import chalk from "chalk";
6
+ import ora from "ora";
7
+ import path from "path";
8
+ import { pushView, getChangedFiles, loadViewMeta } from "../lib/sync.js";
9
+ import { isLoggedIn, getCurrentView } from "../lib/config.js";
10
+
11
+ export async function pushCommand(viewDir?: string): Promise<void> {
12
+ if (!isLoggedIn()) {
13
+ console.log(chalk.red("\n ✗ No estás logueado. Usa: gufi login\n"));
14
+ process.exit(1);
15
+ }
16
+
17
+ console.log(chalk.magenta("\n 🟣 Gufi Push\n"));
18
+
19
+ // Determine view directory
20
+ const dir = viewDir || getCurrentView()?.localPath || process.cwd();
21
+
22
+ // Check if it's a valid Gufi view directory
23
+ const meta = loadViewMeta(dir);
24
+ if (!meta) {
25
+ console.log(chalk.red(" ✗ No es un directorio de vista Gufi válido."));
26
+ console.log(chalk.gray(" Usa: gufi pull <vista> primero\n"));
27
+ process.exit(1);
28
+ }
29
+
30
+ console.log(chalk.gray(` Vista: ${meta.viewName}`));
31
+ console.log(chalk.gray(` Directorio: ${dir}\n`));
32
+
33
+ // Check for changes
34
+ const changedFiles = getChangedFiles(dir);
35
+
36
+ if (changedFiles.length === 0) {
37
+ console.log(chalk.green(" ✓ No hay cambios para subir\n"));
38
+ return;
39
+ }
40
+
41
+ console.log(chalk.gray(" Archivos modificados:"));
42
+ changedFiles.forEach((file) => {
43
+ console.log(chalk.yellow(` • ${file}`));
44
+ });
45
+ console.log();
46
+
47
+ // Push changes
48
+ const spinner = ora("Subiendo cambios...").start();
49
+
50
+ try {
51
+ const result = await pushView(dir);
52
+ spinner.succeed(chalk.green(`${result.pushed} archivo(s) subido(s)`));
53
+ console.log(chalk.gray("\n El preview en Gufi se actualizará automáticamente.\n"));
54
+ } catch (error: any) {
55
+ spinner.fail(chalk.red(error.message));
56
+ process.exit(1);
57
+ }
58
+ }
59
+
60
+ export async function statusCommand(viewDir?: string): Promise<void> {
61
+ const dir = viewDir || getCurrentView()?.localPath || process.cwd();
62
+
63
+ const meta = loadViewMeta(dir);
64
+ if (!meta) {
65
+ console.log(chalk.red("\n ✗ No es un directorio de vista Gufi válido.\n"));
66
+ process.exit(1);
67
+ }
68
+
69
+ console.log(chalk.magenta("\n 🟣 Gufi Status\n"));
70
+ console.log(chalk.gray(` Vista: ${meta.viewName}`));
71
+ console.log(chalk.gray(` View ID: ${meta.viewId}`));
72
+ console.log(chalk.gray(` Último sync: ${meta.lastSync}\n`));
73
+
74
+ const changedFiles = getChangedFiles(dir);
75
+
76
+ if (changedFiles.length === 0) {
77
+ console.log(chalk.green(" ✓ Todo sincronizado\n"));
78
+ } else {
79
+ console.log(chalk.yellow(" Archivos modificados:"));
80
+ changedFiles.forEach((file) => {
81
+ console.log(chalk.yellow(` • ${file}`));
82
+ });
83
+ console.log(chalk.gray("\n Usa: gufi push para subir cambios\n"));
84
+ }
85
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * gufi watch - Auto-sync file changes to Gufi
3
+ */
4
+
5
+ import chalk from "chalk";
6
+ import chokidar from "chokidar";
7
+ import path from "path";
8
+ import { pushSingleFile, loadViewMeta, getViewDir } from "../lib/sync.js";
9
+ import { isLoggedIn, getCurrentView } from "../lib/config.js";
10
+
11
+ export async function watchCommand(viewDir?: string): Promise<void> {
12
+ if (!isLoggedIn()) {
13
+ console.log(chalk.red("\n ✗ No estás logueado. Usa: gufi login\n"));
14
+ process.exit(1);
15
+ }
16
+
17
+ // Determine view directory
18
+ const dir = viewDir || getCurrentView()?.localPath || process.cwd();
19
+
20
+ const meta = loadViewMeta(dir);
21
+ if (!meta) {
22
+ console.log(chalk.red("\n ✗ No es un directorio de vista Gufi válido."));
23
+ console.log(chalk.gray(" Usa: gufi pull <vista> primero\n"));
24
+ process.exit(1);
25
+ }
26
+
27
+ console.log(chalk.magenta("\n 🟣 Gufi Watch\n"));
28
+ console.log(chalk.gray(` Vista: ${meta.viewName}`));
29
+ console.log(chalk.gray(` Directorio: ${dir}\n`));
30
+ console.log(chalk.green(" ✓ Observando cambios...\n"));
31
+ console.log(chalk.gray(" Ctrl+C para salir\n"));
32
+
33
+ // Setup file watcher
34
+ const watcher = chokidar.watch(dir, {
35
+ ignored: [
36
+ /(^|[\/\\])\../, // Hidden files
37
+ "**/node_modules/**",
38
+ "**/.gufi-view.json",
39
+ ],
40
+ persistent: true,
41
+ ignoreInitial: true,
42
+ });
43
+
44
+ // Debounce map to prevent rapid duplicate syncs
45
+ const debounceMap = new Map<string, NodeJS.Timeout>();
46
+
47
+ const syncFile = async (filePath: string) => {
48
+ const relativePath = path.relative(dir, filePath);
49
+
50
+ // Clear existing debounce
51
+ const existing = debounceMap.get(relativePath);
52
+ if (existing) {
53
+ clearTimeout(existing);
54
+ }
55
+
56
+ // Set new debounce
57
+ debounceMap.set(
58
+ relativePath,
59
+ setTimeout(async () => {
60
+ debounceMap.delete(relativePath);
61
+
62
+ try {
63
+ process.stdout.write(chalk.yellow(` ↑ ${relativePath}...`));
64
+ await pushSingleFile(dir, relativePath);
65
+ process.stdout.write(chalk.green(" ✓\n"));
66
+ } catch (error: any) {
67
+ process.stdout.write(chalk.red(` ✗ ${error.message}\n`));
68
+ }
69
+ }, 300)
70
+ );
71
+ };
72
+
73
+ watcher
74
+ .on("change", syncFile)
75
+ .on("add", syncFile)
76
+ .on("error", (error) => {
77
+ console.log(chalk.red(` Error: ${error.message}`));
78
+ });
79
+
80
+ // Handle graceful shutdown
81
+ process.on("SIGINT", () => {
82
+ console.log(chalk.gray("\n\n Deteniendo watch...\n"));
83
+ watcher.close();
84
+ process.exit(0);
85
+ });
86
+
87
+ // Keep process alive
88
+ await new Promise(() => {});
89
+ }