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.
- package/bin/gufi.js +2 -0
- package/dist/commands/login.d.ts +8 -0
- package/dist/commands/login.js +115 -0
- package/dist/commands/pull.d.ts +4 -0
- package/dist/commands/pull.js +96 -0
- package/dist/commands/push.d.ts +5 -0
- package/dist/commands/push.js +70 -0
- package/dist/commands/watch.d.ts +4 -0
- package/dist/commands/watch.js +73 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +55 -0
- package/dist/lib/api.d.ts +35 -0
- package/dist/lib/api.js +83 -0
- package/dist/lib/config.d.ts +26 -0
- package/dist/lib/config.js +68 -0
- package/dist/lib/sync.d.ts +37 -0
- package/dist/lib/sync.js +188 -0
- package/package.json +36 -0
- package/src/commands/login.ts +124 -0
- package/src/commands/pull.ts +113 -0
- package/src/commands/push.ts +85 -0
- package/src/commands/watch.ts +89 -0
- package/src/index.ts +66 -0
- package/src/lib/api.ts +127 -0
- package/src/lib/config.ts +93 -0
- package/src/lib/sync.ts +236 -0
- package/tsconfig.json +17 -0
|
@@ -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>;
|
package/dist/lib/sync.js
ADDED
|
@@ -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
|
+
}
|