gufi-cli 0.1.46 β 0.1.48
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/dist/commands/push.js +14 -1
- package/dist/lib/api.d.ts +10 -1
- package/dist/lib/api.js +13 -2
- package/dist/lib/sync.d.ts +12 -0
- package/dist/lib/sync.js +56 -5
- package/dist/mcp.js +114 -32
- package/package.json +1 -1
package/dist/commands/push.js
CHANGED
|
@@ -6,7 +6,7 @@ import chalk from "chalk";
|
|
|
6
6
|
import ora from "ora";
|
|
7
7
|
import fs from "fs";
|
|
8
8
|
import path from "path";
|
|
9
|
-
import { pushView, getChangedFiles, loadViewMeta } from "../lib/sync.js";
|
|
9
|
+
import { pushView, getChangedFiles, loadViewMeta, checkVersionConflict } from "../lib/sync.js";
|
|
10
10
|
import { isLoggedIn, getCurrentView } from "../lib/config.js";
|
|
11
11
|
import { validateView } from "../lib/api.js";
|
|
12
12
|
import { scanFilesForXSS, hasBlockingErrors } from "../lib/security.js";
|
|
@@ -244,6 +244,19 @@ export async function pushCommand(viewDir) {
|
|
|
244
244
|
console.log(chalk.yellow(` β’ ${file}`));
|
|
245
245
|
});
|
|
246
246
|
console.log();
|
|
247
|
+
// π Version check (BEFORE anything else)
|
|
248
|
+
const versionSpinner = ora("Verificando versiΓ³n...").start();
|
|
249
|
+
const versionCheck = await checkVersionConflict(dir);
|
|
250
|
+
if (versionCheck?.hasConflict) {
|
|
251
|
+
versionSpinner.fail(chalk.red("Conflicto de versiΓ³n"));
|
|
252
|
+
console.log(chalk.red(`\n β ${versionCheck.message}`));
|
|
253
|
+
console.log(chalk.cyan(`\n Haz pull primero: gufi view:pull ${meta.viewId}`));
|
|
254
|
+
console.log(chalk.yellow("\n β οΈ Si tienes cambios locales, cΓ³pialos a otra carpeta antes del pull.\n"));
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
versionSpinner.succeed(chalk.green("VersiΓ³n actualizada"));
|
|
259
|
+
}
|
|
247
260
|
// π Structure validation (BEFORE pushing)
|
|
248
261
|
const structureSpinner = ora("Validando estructura...").start();
|
|
249
262
|
const structureErrors = validateViewStructure(dir);
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -35,8 +35,9 @@ export declare function getViewFiles(viewId: number): Promise<ViewFile[]>;
|
|
|
35
35
|
/**
|
|
36
36
|
* π Full sync: saves all files and deletes any not in the list
|
|
37
37
|
* This is the ONLY way to push - ensures local and server are in sync
|
|
38
|
+
* Backend validates lastPulledSnapshot to prevent version conflicts (409)
|
|
38
39
|
*/
|
|
39
|
-
export declare function saveViewFiles(viewId: number, files: ViewFile[], message?: string): Promise<{
|
|
40
|
+
export declare function saveViewFiles(viewId: number, files: ViewFile[], message?: string, lastPulledSnapshot?: number): Promise<{
|
|
40
41
|
snapshot?: number;
|
|
41
42
|
deleted?: number;
|
|
42
43
|
}>;
|
|
@@ -65,6 +66,14 @@ export declare function validateFiles(files: Array<{
|
|
|
65
66
|
file_path: string;
|
|
66
67
|
content: string;
|
|
67
68
|
}>): Promise<ValidationResult>;
|
|
69
|
+
/**
|
|
70
|
+
* Get the latest snapshot number for a view
|
|
71
|
+
* Used to check if local version is up-to-date before pushing
|
|
72
|
+
*/
|
|
73
|
+
export declare function getLatestSnapshot(viewId: number): Promise<{
|
|
74
|
+
view_id: number;
|
|
75
|
+
latest_snapshot: number;
|
|
76
|
+
}>;
|
|
68
77
|
export interface FileHash {
|
|
69
78
|
hash: string;
|
|
70
79
|
updated_at: string;
|
package/dist/lib/api.js
CHANGED
|
@@ -135,11 +135,12 @@ export async function getViewFiles(viewId) {
|
|
|
135
135
|
/**
|
|
136
136
|
* π Full sync: saves all files and deletes any not in the list
|
|
137
137
|
* This is the ONLY way to push - ensures local and server are in sync
|
|
138
|
+
* Backend validates lastPulledSnapshot to prevent version conflicts (409)
|
|
138
139
|
*/
|
|
139
|
-
export async function saveViewFiles(viewId, files, message) {
|
|
140
|
+
export async function saveViewFiles(viewId, files, message, lastPulledSnapshot) {
|
|
140
141
|
const response = await request(`/api/marketplace/views/${viewId}/files/bulk`, {
|
|
141
142
|
method: "POST",
|
|
142
|
-
body: JSON.stringify({ files, message, sync: true }),
|
|
143
|
+
body: JSON.stringify({ files, message, sync: true, lastPulledSnapshot }),
|
|
143
144
|
});
|
|
144
145
|
return response;
|
|
145
146
|
}
|
|
@@ -172,6 +173,16 @@ export async function validateFiles(files) {
|
|
|
172
173
|
body: JSON.stringify({ files }),
|
|
173
174
|
});
|
|
174
175
|
}
|
|
176
|
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
177
|
+
// π Version Check API - For preventing overwrites
|
|
178
|
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
179
|
+
/**
|
|
180
|
+
* Get the latest snapshot number for a view
|
|
181
|
+
* Used to check if local version is up-to-date before pushing
|
|
182
|
+
*/
|
|
183
|
+
export async function getLatestSnapshot(viewId) {
|
|
184
|
+
return request(`/api/marketplace/views/${viewId}/latest-snapshot`);
|
|
185
|
+
}
|
|
175
186
|
/**
|
|
176
187
|
* Get server-side file hashes for a view
|
|
177
188
|
* Used to compare local vs server content and detect real changes
|
package/dist/lib/sync.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface ViewMeta {
|
|
|
7
7
|
viewName: string;
|
|
8
8
|
packageId: number;
|
|
9
9
|
lastSync: string;
|
|
10
|
+
lastPulledSnapshot?: number;
|
|
10
11
|
files: Record<string, {
|
|
11
12
|
hash: string;
|
|
12
13
|
}>;
|
|
@@ -22,6 +23,7 @@ export declare function loadViewMeta(viewDir: string): ViewMeta | null;
|
|
|
22
23
|
export declare function pullView(viewId: number, viewKey: string, packageId: number): Promise<{
|
|
23
24
|
dir: string;
|
|
24
25
|
fileCount: number;
|
|
26
|
+
snapshot?: number;
|
|
25
27
|
}>;
|
|
26
28
|
/**
|
|
27
29
|
* π Push local files to Gufi (FULL SYNC)
|
|
@@ -33,6 +35,16 @@ export declare function pushView(viewDir?: string, message?: string): Promise<{
|
|
|
33
35
|
deleted: number;
|
|
34
36
|
snapshot?: number;
|
|
35
37
|
}>;
|
|
38
|
+
/**
|
|
39
|
+
* π Check if local version matches server exactly
|
|
40
|
+
* If server snapshot !== local snapshot β conflict (must pull first)
|
|
41
|
+
*/
|
|
42
|
+
export declare function checkVersionConflict(viewDir: string): Promise<{
|
|
43
|
+
hasConflict: boolean;
|
|
44
|
+
localSnapshot?: number;
|
|
45
|
+
serverSnapshot?: number;
|
|
46
|
+
message?: string;
|
|
47
|
+
} | null>;
|
|
36
48
|
/**
|
|
37
49
|
* Check for local changes that need pushing
|
|
38
50
|
*/
|
package/dist/lib/sync.js
CHANGED
|
@@ -6,7 +6,7 @@ import fs from "fs";
|
|
|
6
6
|
import path from "path";
|
|
7
7
|
import os from "os";
|
|
8
8
|
import crypto from "crypto";
|
|
9
|
-
import { getViewFiles, saveViewFiles } from "./api.js";
|
|
9
|
+
import { getViewFiles, saveViewFiles, getLatestSnapshot } from "./api.js";
|
|
10
10
|
import { setCurrentView, getCurrentView } from "./config.js";
|
|
11
11
|
// π Views go to ~/gufi-dev/ for local development
|
|
12
12
|
const GUFI_DEV_DIR = path.join(os.homedir(), "gufi-dev");
|
|
@@ -61,6 +61,15 @@ function saveViewMeta(viewDir, meta) {
|
|
|
61
61
|
export async function pullView(viewId, viewKey, packageId) {
|
|
62
62
|
const viewDir = getViewDir(viewKey);
|
|
63
63
|
ensureDir(viewDir);
|
|
64
|
+
// π Get current snapshot number from server
|
|
65
|
+
let currentSnapshot;
|
|
66
|
+
try {
|
|
67
|
+
const snapshotInfo = await getLatestSnapshot(viewId);
|
|
68
|
+
currentSnapshot = snapshotInfo.latest_snapshot;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// If endpoint not available, continue without snapshot tracking
|
|
72
|
+
}
|
|
64
73
|
const files = await getViewFiles(viewId);
|
|
65
74
|
const fileMeta = {};
|
|
66
75
|
for (const file of files) {
|
|
@@ -73,15 +82,16 @@ export async function pullView(viewId, viewKey, packageId) {
|
|
|
73
82
|
}
|
|
74
83
|
const meta = {
|
|
75
84
|
viewId,
|
|
76
|
-
viewName: viewKey,
|
|
85
|
+
viewName: viewKey,
|
|
77
86
|
packageId,
|
|
78
87
|
lastSync: new Date().toISOString(),
|
|
88
|
+
lastPulledSnapshot: currentSnapshot, // π Must match server exactly to push
|
|
79
89
|
files: fileMeta,
|
|
80
90
|
};
|
|
81
91
|
saveViewMeta(viewDir, meta);
|
|
82
92
|
// Update current view in config
|
|
83
93
|
setCurrentView({ id: viewId, name: viewKey, packageId, localPath: viewDir });
|
|
84
|
-
return { dir: viewDir, fileCount: files.length };
|
|
94
|
+
return { dir: viewDir, fileCount: files.length, snapshot: currentSnapshot };
|
|
85
95
|
}
|
|
86
96
|
/**
|
|
87
97
|
* π Push local files to Gufi (FULL SYNC)
|
|
@@ -113,10 +123,14 @@ export async function pushView(viewDir, message) {
|
|
|
113
123
|
newMeta[filePath] = { hash: hashContent(content) };
|
|
114
124
|
}
|
|
115
125
|
// Push ALL files - backend handles sync (deletes files not in request)
|
|
116
|
-
|
|
117
|
-
|
|
126
|
+
// π Send lastPulledSnapshot for server-side version check (returns 409 on conflict)
|
|
127
|
+
const result = await saveViewFiles(meta.viewId, allFiles, message, meta.lastPulledSnapshot);
|
|
128
|
+
// Update meta with new file list and snapshot
|
|
118
129
|
meta.files = newMeta;
|
|
119
130
|
meta.lastSync = new Date().toISOString();
|
|
131
|
+
if (result.snapshot) {
|
|
132
|
+
meta.lastPulledSnapshot = result.snapshot; // π Update to new snapshot
|
|
133
|
+
}
|
|
120
134
|
saveViewMeta(dir, meta);
|
|
121
135
|
return { pushed: allFiles.length, deleted: result.deleted || 0, snapshot: result.snapshot };
|
|
122
136
|
}
|
|
@@ -139,6 +153,43 @@ function getLocalFiles(dir, prefix = "") {
|
|
|
139
153
|
}
|
|
140
154
|
return files;
|
|
141
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* π Check if local version matches server exactly
|
|
158
|
+
* If server snapshot !== local snapshot β conflict (must pull first)
|
|
159
|
+
*/
|
|
160
|
+
export async function checkVersionConflict(viewDir) {
|
|
161
|
+
const meta = loadViewMeta(viewDir);
|
|
162
|
+
if (!meta)
|
|
163
|
+
return null;
|
|
164
|
+
try {
|
|
165
|
+
const serverInfo = await getLatestSnapshot(meta.viewId);
|
|
166
|
+
const serverSnapshot = serverInfo.latest_snapshot;
|
|
167
|
+
// π If server has versions but we don't have local snapshot tracking,
|
|
168
|
+
// it means we pulled with an old CLI version - require pull first
|
|
169
|
+
if (serverSnapshot > 0 && meta.lastPulledSnapshot === undefined) {
|
|
170
|
+
return {
|
|
171
|
+
hasConflict: true,
|
|
172
|
+
localSnapshot: undefined,
|
|
173
|
+
serverSnapshot,
|
|
174
|
+
message: `Tu versiΓ³n local no tiene tracking de versiones. El servidor estΓ‘ en v${serverSnapshot}. Haz pull primero.`
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
// π If snapshots don't match, someone else pushed - require pull first
|
|
178
|
+
if (meta.lastPulledSnapshot !== undefined && serverSnapshot !== meta.lastPulledSnapshot) {
|
|
179
|
+
return {
|
|
180
|
+
hasConflict: true,
|
|
181
|
+
localSnapshot: meta.lastPulledSnapshot,
|
|
182
|
+
serverSnapshot,
|
|
183
|
+
message: `Tu versiΓ³n local (v${meta.lastPulledSnapshot}) no coincide con el servidor (v${serverSnapshot}). Haz pull primero.`
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return { hasConflict: false, localSnapshot: meta.lastPulledSnapshot, serverSnapshot };
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// If can't check, allow push (backwards compat)
|
|
190
|
+
return { hasConflict: false };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
142
193
|
/**
|
|
143
194
|
* Check for local changes that need pushing
|
|
144
195
|
*/
|
package/dist/mcp.js
CHANGED
|
@@ -37,6 +37,14 @@ function canWriteLocal() {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
import { getToken, getRefreshToken, loadConfig, isLoggedIn, getTokenForEnv, getRefreshTokenForEnv, setTokenForEnv, getCredentialsForEnv, getCurrentEnv } from "./lib/config.js";
|
|
40
|
+
// π Valid column types (hardcoded to avoid npm dependency on @gufi/column-types)
|
|
41
|
+
const COLUMN_TYPE_NAMES = [
|
|
42
|
+
"text", "email", "url", "barcode", "signature", "number", "number_int",
|
|
43
|
+
"number_float", "percentage", "date", "time", "created_at", "select",
|
|
44
|
+
"multiselect", "relation", "reversed_relation", "users", "boolean",
|
|
45
|
+
"currency", "phone", "location", "file", "json", "formula", "mirror",
|
|
46
|
+
"custom", "password"
|
|
47
|
+
];
|
|
40
48
|
// For ES modules __dirname equivalent
|
|
41
49
|
const __filename = fileURLToPath(import.meta.url);
|
|
42
50
|
const __dirname = path.dirname(__filename);
|
|
@@ -870,7 +878,11 @@ Example: gufi_view_pull({ view_id: 13 })`,
|
|
|
870
878
|
description: `π€ Upload local view changes to draft.
|
|
871
879
|
|
|
872
880
|
Pushes changed files from ~/gufi-dev/view_<id>/ to Gufi draft.
|
|
873
|
-
Creates a snapshot for version history (last
|
|
881
|
+
Creates a snapshot for version history (last 1000 kept).
|
|
882
|
+
|
|
883
|
+
β οΈ IMPORTANT: Checks if local version is up-to-date before pushing.
|
|
884
|
+
If server has newer version, returns VERSION_CONFLICT error.
|
|
885
|
+
You must pull the latest version first before pushing.
|
|
874
886
|
|
|
875
887
|
After pushing, use: gufi package:publish <package_id> to publish.
|
|
876
888
|
|
|
@@ -1182,37 +1194,18 @@ const toolHandlers = {
|
|
|
1182
1194
|
// gufi_context({ company_id: "X", module_id: "Y" })
|
|
1183
1195
|
// gufi_context({ company_id: "X", entity_id: "Z" })
|
|
1184
1196
|
async gufi_schema_modify(params) {
|
|
1185
|
-
// π
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
int: "number_int",
|
|
1191
|
-
integer: "number_int",
|
|
1192
|
-
float: "number_float",
|
|
1193
|
-
decimal: "number_float",
|
|
1194
|
-
double: "number_float",
|
|
1195
|
-
bool: "boolean",
|
|
1196
|
-
timestamp: "datetime",
|
|
1197
|
-
money: "currency",
|
|
1198
|
-
tel: "phone",
|
|
1199
|
-
address: "location",
|
|
1200
|
-
};
|
|
1201
|
-
// Process operations: normalize field types
|
|
1202
|
-
const normalizedOps = params.operations.map((op) => {
|
|
1203
|
-
if (op.field?.type && TYPE_ALIASES[op.field.type]) {
|
|
1204
|
-
return {
|
|
1205
|
-
...op,
|
|
1206
|
-
field: { ...op.field, type: TYPE_ALIASES[op.field.type] },
|
|
1207
|
-
};
|
|
1197
|
+
// π Validate field types BEFORE sending to API
|
|
1198
|
+
// Uses COLUMN_TYPE_NAMES from @gufi/column-types (single source of truth)
|
|
1199
|
+
for (const op of params.operations) {
|
|
1200
|
+
if (op.field?.type && !COLUMN_TYPE_NAMES.includes(op.field.type)) {
|
|
1201
|
+
throw new Error(`Invalid field type: "${op.field.type}". Valid types: ${COLUMN_TYPE_NAMES.join(", ")}`);
|
|
1208
1202
|
}
|
|
1209
|
-
|
|
1210
|
-
});
|
|
1203
|
+
}
|
|
1211
1204
|
// Use the new /api/schema/operations endpoint for semantic operations
|
|
1212
1205
|
const result = await apiRequest("/api/schema/operations", {
|
|
1213
1206
|
method: "POST",
|
|
1214
1207
|
body: JSON.stringify({
|
|
1215
|
-
operations:
|
|
1208
|
+
operations: params.operations,
|
|
1216
1209
|
preview: params.preview || false,
|
|
1217
1210
|
skip_existing: params.skip_existing || false,
|
|
1218
1211
|
}),
|
|
@@ -1824,6 +1817,15 @@ const toolHandlers = {
|
|
|
1824
1817
|
// π CLI: Save to ~/gufi-dev/view_<id>/
|
|
1825
1818
|
const viewDir = path.join(LOCAL_VIEWS_DIR, `view_${viewId}`);
|
|
1826
1819
|
fs.mkdirSync(viewDir, { recursive: true });
|
|
1820
|
+
// π Get latest snapshot for version tracking
|
|
1821
|
+
let latestSnapshot;
|
|
1822
|
+
try {
|
|
1823
|
+
const snapshotResp = await apiRequest(`/api/marketplace/views/${viewId}/latest-snapshot`, {}, companyId, true, env);
|
|
1824
|
+
latestSnapshot = snapshotResp.latest_snapshot;
|
|
1825
|
+
}
|
|
1826
|
+
catch {
|
|
1827
|
+
// If endpoint not available, continue without snapshot tracking
|
|
1828
|
+
}
|
|
1827
1829
|
const fileMeta = {};
|
|
1828
1830
|
for (const file of files) {
|
|
1829
1831
|
const filePath = path.join(viewDir, file.file_path.replace(/^\//, ""));
|
|
@@ -1833,12 +1835,13 @@ const toolHandlers = {
|
|
|
1833
1835
|
hash: crypto.createHash("sha256").update(file.content, "utf-8").digest("hex")
|
|
1834
1836
|
};
|
|
1835
1837
|
}
|
|
1836
|
-
// Save metadata
|
|
1838
|
+
// Save metadata (with snapshot for version tracking)
|
|
1837
1839
|
const meta = {
|
|
1838
1840
|
viewId,
|
|
1839
1841
|
viewName: `view_${viewId}`,
|
|
1840
1842
|
packageId: view.package_id || 0,
|
|
1841
1843
|
lastSync: new Date().toISOString(),
|
|
1844
|
+
lastPulledSnapshot: latestSnapshot, // π For version conflict detection
|
|
1842
1845
|
files: fileMeta,
|
|
1843
1846
|
};
|
|
1844
1847
|
fs.writeFileSync(path.join(viewDir, ".gufi-view.json"), JSON.stringify(meta, null, 2));
|
|
@@ -1881,12 +1884,24 @@ const toolHandlers = {
|
|
|
1881
1884
|
throw new Error("view_id is required");
|
|
1882
1885
|
}
|
|
1883
1886
|
let files = [];
|
|
1887
|
+
let lastPulledSnapshot; // π For version conflict detection
|
|
1884
1888
|
if (useLocal) {
|
|
1885
1889
|
// π CLI: Read from ~/gufi-dev/view_<id>/
|
|
1886
1890
|
const viewDir = path.join(LOCAL_VIEWS_DIR, `view_${viewId}`);
|
|
1887
1891
|
if (!fs.existsSync(viewDir)) {
|
|
1888
1892
|
throw new Error(`View directory not found: ${viewDir}. Run gufi_view_pull first.`);
|
|
1889
1893
|
}
|
|
1894
|
+
// π Read metadata for version tracking (backend handles the check)
|
|
1895
|
+
const metaPath = path.join(viewDir, ".gufi-view.json");
|
|
1896
|
+
if (fs.existsSync(metaPath)) {
|
|
1897
|
+
try {
|
|
1898
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
1899
|
+
lastPulledSnapshot = meta.lastPulledSnapshot;
|
|
1900
|
+
}
|
|
1901
|
+
catch {
|
|
1902
|
+
// Ignore metadata read errors
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1890
1905
|
// Get all files recursively
|
|
1891
1906
|
const getFiles = (dir, prefix = "") => {
|
|
1892
1907
|
const result = [];
|
|
@@ -1921,16 +1936,83 @@ const toolHandlers = {
|
|
|
1921
1936
|
const workspaceResponse = await apiRequest(`/api/claude/workspace/view/${viewId}/files`, {}, undefined, true, env);
|
|
1922
1937
|
files = workspaceResponse.data || workspaceResponse || [];
|
|
1923
1938
|
}
|
|
1939
|
+
// π VERSION CHECK: Always verify before push (don't rely on backend alone)
|
|
1940
|
+
// This prevents overwriting changes from other developers
|
|
1941
|
+
try {
|
|
1942
|
+
const snapshotResp = await apiRequest(`/api/marketplace/views/${viewId}/latest-snapshot`, {}, undefined, true, env);
|
|
1943
|
+
const serverSnapshot = snapshotResp.latest_snapshot;
|
|
1944
|
+
// If server has versions but we don't have a local snapshot β conflict
|
|
1945
|
+
// (means we pulled with an old CLI that didn't track versions)
|
|
1946
|
+
if (serverSnapshot > 0 && lastPulledSnapshot === undefined) {
|
|
1947
|
+
return {
|
|
1948
|
+
success: false,
|
|
1949
|
+
error: "VERSION_CONFLICT",
|
|
1950
|
+
message: `Tu versiΓ³n local no tiene tracking de versiones. El servidor estΓ‘ en v${serverSnapshot}.`,
|
|
1951
|
+
local_snapshot: null,
|
|
1952
|
+
server_snapshot: serverSnapshot,
|
|
1953
|
+
_hint: `Haz pull primero para sincronizar: gufi_view_pull({ view_id: ${viewId} }). Si tienes cambios locales, cΓ³pialos a otra carpeta antes del pull.`,
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
// If snapshots don't match β someone else pushed
|
|
1957
|
+
if (lastPulledSnapshot !== undefined && serverSnapshot !== lastPulledSnapshot) {
|
|
1958
|
+
return {
|
|
1959
|
+
success: false,
|
|
1960
|
+
error: "VERSION_CONFLICT",
|
|
1961
|
+
message: `Tu versiΓ³n local (v${lastPulledSnapshot}) no coincide con el servidor (v${serverSnapshot}). Alguien mΓ‘s hizo push.`,
|
|
1962
|
+
local_snapshot: lastPulledSnapshot,
|
|
1963
|
+
server_snapshot: serverSnapshot,
|
|
1964
|
+
_hint: `Haz pull primero: gufi_view_pull({ view_id: ${viewId} }). Si tienes cambios locales, cΓ³pialos a otra carpeta antes del pull.`,
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
catch (err) {
|
|
1969
|
+
// If we can't check version, log warning but continue (backwards compat)
|
|
1970
|
+
console.error("Warning: Could not verify version before push:", err.message);
|
|
1971
|
+
}
|
|
1924
1972
|
// π Check for large files (>1000 lines) and suggest splitting
|
|
1925
1973
|
const LINE_LIMIT = 1000;
|
|
1926
1974
|
const largeFiles = files
|
|
1927
1975
|
.map(f => ({ path: f.file_path, lines: f.content.split('\n').length }))
|
|
1928
1976
|
.filter(f => f.lines > LINE_LIMIT);
|
|
1929
1977
|
// Push to server using apiRequest (supports env)
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1978
|
+
// π Backend will check lastPulledSnapshot and return 409 if conflict
|
|
1979
|
+
let result;
|
|
1980
|
+
try {
|
|
1981
|
+
result = await apiRequest(`/api/marketplace/views/${viewId}/files/bulk`, {
|
|
1982
|
+
method: "POST",
|
|
1983
|
+
body: JSON.stringify({ files, message: params.message, sync: true, lastPulledSnapshot }),
|
|
1984
|
+
}, undefined, true, env);
|
|
1985
|
+
}
|
|
1986
|
+
catch (err) {
|
|
1987
|
+
// π Handle version conflict from backend
|
|
1988
|
+
if (err.status === 409 || err.error === "VERSION_CONFLICT") {
|
|
1989
|
+
return {
|
|
1990
|
+
success: false,
|
|
1991
|
+
error: "VERSION_CONFLICT",
|
|
1992
|
+
message: err.message || `Conflicto de versiΓ³n. Haz pull primero.`,
|
|
1993
|
+
local_snapshot: err.localSnapshot,
|
|
1994
|
+
server_snapshot: err.serverSnapshot,
|
|
1995
|
+
_hint: `Haz pull primero: gufi_view_pull({ view_id: ${viewId} }). Si tienes cambios locales, cΓ³pialos a otra carpeta antes del pull.`,
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
throw err;
|
|
1999
|
+
}
|
|
2000
|
+
// π Update local metadata with new snapshot number (for version tracking)
|
|
2001
|
+
if (useLocal && result.snapshot) {
|
|
2002
|
+
const viewDir = path.join(LOCAL_VIEWS_DIR, `view_${viewId}`);
|
|
2003
|
+
const metaPath = path.join(viewDir, ".gufi-view.json");
|
|
2004
|
+
if (fs.existsSync(metaPath)) {
|
|
2005
|
+
try {
|
|
2006
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
2007
|
+
meta.lastPulledSnapshot = result.snapshot;
|
|
2008
|
+
meta.lastSync = new Date().toISOString();
|
|
2009
|
+
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
|
|
2010
|
+
}
|
|
2011
|
+
catch {
|
|
2012
|
+
// Ignore metadata update errors
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
1934
2016
|
// Get view info for package
|
|
1935
2017
|
let packageInfo = null;
|
|
1936
2018
|
const viewResponse = await apiRequest(`/api/marketplace/views/${viewId}`, {}, undefined, true, env);
|