@xano/cli 0.0.95 → 1.0.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/README.md +133 -66
- package/dist/base-command.d.ts +41 -1
- package/dist/base-command.js +92 -3
- package/dist/commands/auth/index.d.ts +1 -0
- package/dist/commands/auth/index.js +16 -11
- package/dist/commands/branch/create/index.d.ts +4 -1
- package/dist/commands/branch/create/index.js +22 -21
- package/dist/commands/branch/delete/index.d.ts +1 -0
- package/dist/commands/branch/delete/index.js +1 -4
- package/dist/commands/branch/edit/index.d.ts +1 -0
- package/dist/commands/branch/edit/index.js +1 -4
- package/dist/commands/branch/get/index.d.ts +1 -0
- package/dist/commands/branch/get/index.js +1 -4
- package/dist/commands/branch/list/index.d.ts +2 -6
- package/dist/commands/branch/list/index.js +13 -17
- package/dist/commands/branch/set_live/index.d.ts +1 -0
- package/dist/commands/branch/set_live/index.js +1 -4
- package/dist/commands/function/create/index.d.ts +1 -0
- package/dist/commands/function/create/index.js +1 -2
- package/dist/commands/function/edit/index.d.ts +1 -0
- package/dist/commands/function/edit/index.js +1 -2
- package/dist/commands/function/get/index.d.ts +1 -0
- package/dist/commands/function/get/index.js +1 -4
- package/dist/commands/function/list/index.d.ts +1 -0
- package/dist/commands/function/list/index.js +1 -4
- package/dist/commands/platform/get/index.d.ts +1 -0
- package/dist/commands/platform/get/index.js +1 -4
- package/dist/commands/platform/list/index.d.ts +1 -0
- package/dist/commands/platform/list/index.js +1 -4
- package/dist/commands/profile/create/index.d.ts +1 -0
- package/dist/commands/profile/create/index.js +12 -6
- package/dist/commands/profile/delete/index.d.ts +1 -0
- package/dist/commands/profile/delete/index.js +8 -4
- package/dist/commands/profile/edit/index.d.ts +1 -0
- package/dist/commands/profile/edit/index.js +3 -6
- package/dist/commands/profile/get/index.d.ts +3 -0
- package/dist/commands/profile/get/index.js +12 -5
- package/dist/commands/profile/list/index.d.ts +1 -0
- package/dist/commands/profile/list/index.js +8 -4
- package/dist/commands/profile/me/index.d.ts +1 -0
- package/dist/commands/profile/me/index.js +22 -6
- package/dist/commands/profile/set/index.d.ts +3 -0
- package/dist/commands/profile/set/index.js +12 -6
- package/dist/commands/profile/token/index.d.ts +3 -0
- package/dist/commands/profile/token/index.js +12 -5
- package/dist/commands/profile/wizard/index.d.ts +1 -0
- package/dist/commands/profile/wizard/index.js +16 -12
- package/dist/commands/profile/workspace/index.d.ts +3 -0
- package/dist/commands/profile/workspace/index.js +12 -5
- package/dist/commands/profile/workspace/set/index.d.ts +1 -0
- package/dist/commands/profile/workspace/set/index.js +2 -4
- package/dist/commands/release/create/index.d.ts +4 -1
- package/dist/commands/release/create/index.js +12 -14
- package/dist/commands/release/delete/index.d.ts +1 -0
- package/dist/commands/release/delete/index.js +1 -4
- package/dist/commands/release/deploy/index.d.ts +20 -0
- package/dist/commands/release/deploy/index.js +137 -0
- package/dist/commands/release/edit/index.d.ts +1 -0
- package/dist/commands/release/edit/index.js +1 -4
- package/dist/commands/release/export/index.d.ts +1 -0
- package/dist/commands/release/export/index.js +1 -3
- package/dist/commands/release/get/index.d.ts +1 -0
- package/dist/commands/release/get/index.js +1 -4
- package/dist/commands/release/import/index.d.ts +1 -0
- package/dist/commands/release/import/index.js +1 -3
- package/dist/commands/release/list/index.d.ts +1 -0
- package/dist/commands/release/list/index.js +1 -4
- package/dist/commands/release/pull/index.d.ts +2 -3
- package/dist/commands/release/pull/index.js +19 -18
- package/dist/commands/release/push/index.d.ts +2 -3
- package/dist/commands/release/push/index.js +19 -22
- package/dist/commands/sandbox/delete/index.d.ts +13 -0
- package/dist/commands/sandbox/delete/index.js +71 -0
- package/dist/commands/sandbox/env/delete/index.d.ts +15 -0
- package/dist/commands/sandbox/env/delete/index.js +89 -0
- package/dist/commands/sandbox/env/get/index.d.ts +13 -0
- package/dist/commands/sandbox/env/get/index.js +65 -0
- package/dist/commands/sandbox/env/get_all/index.d.ts +14 -0
- package/dist/commands/sandbox/env/get_all/index.js +78 -0
- package/dist/commands/sandbox/env/list/index.d.ts +12 -0
- package/dist/commands/sandbox/env/list/index.js +67 -0
- package/dist/commands/sandbox/env/set/index.d.ts +14 -0
- package/dist/commands/sandbox/env/set/index.js +74 -0
- package/dist/commands/sandbox/env/set_all/index.d.ts +14 -0
- package/dist/commands/sandbox/env/set_all/index.js +86 -0
- package/dist/commands/sandbox/get/index.d.ts +12 -0
- package/dist/commands/sandbox/get/index.js +63 -0
- package/dist/commands/sandbox/impersonate/index.d.ts +5 -0
- package/dist/commands/sandbox/impersonate/index.js +5 -0
- package/dist/commands/sandbox/license/get/index.d.ts +14 -0
- package/dist/commands/sandbox/license/get/index.js +78 -0
- package/dist/commands/sandbox/license/set/index.d.ts +15 -0
- package/dist/commands/sandbox/license/set/index.js +95 -0
- package/dist/commands/sandbox/pull/index.d.ts +16 -0
- package/dist/commands/sandbox/pull/index.js +185 -0
- package/dist/commands/sandbox/push/index.d.ts +26 -0
- package/dist/commands/sandbox/push/index.js +196 -0
- package/dist/commands/sandbox/reset/index.d.ts +13 -0
- package/dist/commands/sandbox/reset/index.js +71 -0
- package/dist/commands/sandbox/review/index.d.ts +14 -0
- package/dist/commands/sandbox/review/index.js +94 -0
- package/dist/commands/sandbox/unit_test/list/index.d.ts +14 -0
- package/dist/commands/sandbox/unit_test/list/index.js +91 -0
- package/dist/commands/sandbox/unit_test/run/index.d.ts +15 -0
- package/dist/commands/sandbox/unit_test/run/index.js +79 -0
- package/dist/commands/sandbox/unit_test/run_all/index.d.ts +14 -0
- package/dist/commands/sandbox/unit_test/run_all/index.js +171 -0
- package/dist/commands/sandbox/workflow_test/list/index.d.ts +13 -0
- package/dist/commands/sandbox/workflow_test/list/index.js +84 -0
- package/dist/commands/sandbox/workflow_test/run/index.d.ts +18 -0
- package/dist/commands/sandbox/workflow_test/run/index.js +77 -0
- package/dist/commands/sandbox/workflow_test/run_all/index.d.ts +13 -0
- package/dist/commands/sandbox/workflow_test/run_all/index.js +157 -0
- package/dist/commands/static_host/build/create/index.d.ts +1 -0
- package/dist/commands/static_host/build/create/index.js +1 -3
- package/dist/commands/static_host/build/get/index.d.ts +1 -0
- package/dist/commands/static_host/build/get/index.js +1 -4
- package/dist/commands/static_host/build/list/index.d.ts +1 -0
- package/dist/commands/static_host/build/list/index.js +1 -4
- package/dist/commands/static_host/list/index.d.ts +1 -0
- package/dist/commands/static_host/list/index.js +1 -4
- package/dist/commands/tenant/backup/create/index.d.ts +1 -0
- package/dist/commands/tenant/backup/create/index.js +1 -4
- package/dist/commands/tenant/backup/delete/index.d.ts +1 -0
- package/dist/commands/tenant/backup/delete/index.js +1 -4
- package/dist/commands/tenant/backup/export/index.d.ts +1 -0
- package/dist/commands/tenant/backup/export/index.js +1 -3
- package/dist/commands/tenant/backup/import/index.d.ts +1 -0
- package/dist/commands/tenant/backup/import/index.js +1 -3
- package/dist/commands/tenant/backup/list/index.d.ts +1 -0
- package/dist/commands/tenant/backup/list/index.js +1 -4
- package/dist/commands/tenant/backup/restore/index.d.ts +1 -0
- package/dist/commands/tenant/backup/restore/index.js +1 -4
- package/dist/commands/tenant/cluster/create/index.d.ts +1 -0
- package/dist/commands/tenant/cluster/create/index.js +1 -3
- package/dist/commands/tenant/cluster/delete/index.d.ts +1 -0
- package/dist/commands/tenant/cluster/delete/index.js +1 -4
- package/dist/commands/tenant/cluster/edit/index.d.ts +1 -0
- package/dist/commands/tenant/cluster/edit/index.js +1 -4
- package/dist/commands/tenant/cluster/get/index.d.ts +1 -0
- package/dist/commands/tenant/cluster/get/index.js +1 -4
- package/dist/commands/tenant/cluster/license/get/index.d.ts +1 -0
- package/dist/commands/tenant/cluster/license/get/index.js +1 -3
- package/dist/commands/tenant/cluster/license/set/index.d.ts +1 -0
- package/dist/commands/tenant/cluster/license/set/index.js +1 -3
- package/dist/commands/tenant/cluster/list/index.d.ts +1 -0
- package/dist/commands/tenant/cluster/list/index.js +1 -4
- package/dist/commands/tenant/create/index.d.ts +1 -1
- package/dist/commands/tenant/create/index.js +1 -8
- package/dist/commands/tenant/delete/index.d.ts +1 -0
- package/dist/commands/tenant/delete/index.js +1 -4
- package/dist/commands/tenant/deploy_platform/index.d.ts +1 -0
- package/dist/commands/tenant/deploy_platform/index.js +1 -3
- package/dist/commands/tenant/deploy_release/index.d.ts +1 -0
- package/dist/commands/tenant/deploy_release/index.js +1 -4
- package/dist/commands/tenant/edit/index.d.ts +1 -0
- package/dist/commands/tenant/edit/index.js +1 -4
- package/dist/commands/tenant/env/delete/index.d.ts +1 -0
- package/dist/commands/tenant/env/delete/index.js +1 -4
- package/dist/commands/tenant/env/get/index.d.ts +1 -0
- package/dist/commands/tenant/env/get/index.js +1 -4
- package/dist/commands/tenant/env/get_all/index.d.ts +1 -0
- package/dist/commands/tenant/env/get_all/index.js +1 -3
- package/dist/commands/tenant/env/list/index.d.ts +1 -0
- package/dist/commands/tenant/env/list/index.js +1 -4
- package/dist/commands/tenant/env/set/index.d.ts +1 -0
- package/dist/commands/tenant/env/set/index.js +1 -4
- package/dist/commands/tenant/env/set_all/index.d.ts +1 -0
- package/dist/commands/tenant/env/set_all/index.js +1 -3
- package/dist/commands/tenant/get/index.d.ts +1 -0
- package/dist/commands/tenant/get/index.js +3 -6
- package/dist/commands/tenant/impersonate/index.d.ts +1 -0
- package/dist/commands/tenant/impersonate/index.js +1 -4
- package/dist/commands/tenant/license/get/index.d.ts +1 -0
- package/dist/commands/tenant/license/get/index.js +1 -3
- package/dist/commands/tenant/license/set/index.d.ts +1 -0
- package/dist/commands/tenant/license/set/index.js +1 -3
- package/dist/commands/tenant/list/index.d.ts +1 -0
- package/dist/commands/tenant/list/index.js +3 -6
- package/dist/commands/tenant/pull/index.d.ts +2 -3
- package/dist/commands/tenant/pull/index.js +20 -21
- package/dist/commands/tenant/push/index.d.ts +2 -22
- package/dist/commands/tenant/push/index.js +7 -259
- package/dist/commands/tenant/unit_test/list/index.d.ts +16 -0
- package/dist/commands/tenant/unit_test/list/index.js +115 -0
- package/dist/commands/tenant/unit_test/run/index.d.ts +17 -0
- package/dist/commands/tenant/unit_test/run/index.js +103 -0
- package/dist/commands/tenant/unit_test/run_all/index.d.ts +16 -0
- package/dist/commands/tenant/unit_test/run_all/index.js +190 -0
- package/dist/commands/tenant/workflow_test/list/index.d.ts +15 -0
- package/dist/commands/tenant/workflow_test/list/index.js +108 -0
- package/dist/commands/tenant/workflow_test/run/index.d.ts +20 -0
- package/dist/commands/tenant/workflow_test/run/index.js +101 -0
- package/dist/commands/tenant/workflow_test/run_all/index.d.ts +15 -0
- package/dist/commands/tenant/workflow_test/run_all/index.js +176 -0
- package/dist/commands/unit_test/list/index.d.ts +1 -0
- package/dist/commands/unit_test/list/index.js +1 -4
- package/dist/commands/unit_test/run/index.d.ts +1 -0
- package/dist/commands/unit_test/run/index.js +1 -4
- package/dist/commands/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/unit_test/run_all/index.js +1 -4
- package/dist/commands/update/index.d.ts +1 -0
- package/dist/commands/workflow_test/delete/index.d.ts +1 -0
- package/dist/commands/workflow_test/delete/index.js +1 -4
- package/dist/commands/workflow_test/get/index.d.ts +1 -0
- package/dist/commands/workflow_test/get/index.js +1 -4
- package/dist/commands/workflow_test/list/index.d.ts +1 -0
- package/dist/commands/workflow_test/list/index.js +1 -4
- package/dist/commands/workflow_test/run/index.d.ts +1 -0
- package/dist/commands/workflow_test/run/index.js +1 -4
- package/dist/commands/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/workflow_test/run_all/index.js +1 -4
- package/dist/commands/workspace/create/index.d.ts +1 -0
- package/dist/commands/workspace/create/index.js +1 -4
- package/dist/commands/workspace/delete/index.d.ts +2 -6
- package/dist/commands/workspace/delete/index.js +17 -16
- package/dist/commands/workspace/edit/index.d.ts +3 -6
- package/dist/commands/workspace/edit/index.js +31 -25
- package/dist/commands/workspace/get/index.d.ts +2 -6
- package/dist/commands/workspace/get/index.js +23 -25
- package/dist/commands/workspace/git/pull/index.d.ts +2 -3
- package/dist/commands/workspace/git/pull/index.js +18 -17
- package/dist/commands/workspace/list/index.d.ts +2 -0
- package/dist/commands/workspace/list/index.js +15 -11
- package/dist/commands/workspace/pull/index.d.ts +2 -3
- package/dist/commands/workspace/pull/index.js +21 -24
- package/dist/commands/workspace/push/index.d.ts +7 -16
- package/dist/commands/workspace/push/index.js +85 -674
- package/dist/help.d.ts +2 -1
- package/dist/help.js +39 -1
- package/dist/utils/multidoc-push.d.ts +63 -0
- package/dist/utils/multidoc-push.js +690 -0
- package/dist/utils/reference-checker.d.ts +57 -0
- package/dist/utils/reference-checker.js +232 -0
- package/oclif.manifest.json +5631 -2297
- package/package.json +17 -2
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
import { ux } from '@oclif/core';
|
|
2
|
+
import { minimatch } from 'minimatch';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import { join, relative } from 'node:path';
|
|
5
|
+
import { buildDocumentKey, findFilesWithGuid, parseDocument } from './document-parser.js';
|
|
6
|
+
import { checkReferences, checkTableIndexes } from './reference-checker.js';
|
|
7
|
+
// ── File Collection ─────────────────────────────────────────────────────────
|
|
8
|
+
/**
|
|
9
|
+
* Recursively collect all .xs files from a directory, sorted for deterministic ordering.
|
|
10
|
+
*/
|
|
11
|
+
export function collectFiles(dir) {
|
|
12
|
+
const files = [];
|
|
13
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
const fullPath = join(dir, entry.name);
|
|
16
|
+
if (entry.isDirectory()) {
|
|
17
|
+
files.push(...collectFiles(fullPath));
|
|
18
|
+
}
|
|
19
|
+
else if (entry.isFile() && entry.name.endsWith('.xs')) {
|
|
20
|
+
files.push(fullPath);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return files.sort();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Apply include/exclude glob filters to a file list. Logs filter results.
|
|
27
|
+
* Returns the filtered file list.
|
|
28
|
+
*/
|
|
29
|
+
export function applyFilters(files, inputDir, include, exclude, log) {
|
|
30
|
+
let filtered = files;
|
|
31
|
+
const totalCount = files.length;
|
|
32
|
+
if (include && include.length > 0) {
|
|
33
|
+
filtered = filtered.filter((f) => {
|
|
34
|
+
const rel = relative(inputDir, f);
|
|
35
|
+
return include.some((pattern) => minimatch(rel, pattern, { matchBase: true }));
|
|
36
|
+
});
|
|
37
|
+
log('');
|
|
38
|
+
log(` ${ux.colorize('dim', 'Include:')} ${include.map((p) => ux.colorize('cyan', p)).join(', ')}`);
|
|
39
|
+
log(` ${ux.colorize('dim', 'Matched:')} ${ux.colorize('bold', String(filtered.length))} of ${totalCount} files`);
|
|
40
|
+
}
|
|
41
|
+
if (exclude && exclude.length > 0) {
|
|
42
|
+
const beforeCount = filtered.length;
|
|
43
|
+
filtered = filtered.filter((f) => {
|
|
44
|
+
const rel = relative(inputDir, f);
|
|
45
|
+
return !exclude.some((pattern) => minimatch(rel, pattern, { matchBase: true }));
|
|
46
|
+
});
|
|
47
|
+
log('');
|
|
48
|
+
log(` ${ux.colorize('dim', 'Exclude:')} ${exclude.map((p) => ux.colorize('cyan', p)).join(', ')}`);
|
|
49
|
+
log(` ${ux.colorize('dim', 'Kept:')} ${ux.colorize('bold', String(filtered.length))} of ${beforeCount} files (excluded ${beforeCount - filtered.length})`);
|
|
50
|
+
}
|
|
51
|
+
return filtered;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Read .xs files into document entries, skipping empty files.
|
|
55
|
+
*/
|
|
56
|
+
export function readDocuments(files) {
|
|
57
|
+
const entries = [];
|
|
58
|
+
for (const filePath of files) {
|
|
59
|
+
const content = fs.readFileSync(filePath, 'utf8').trim();
|
|
60
|
+
if (content) {
|
|
61
|
+
entries.push({ content, filePath });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return entries;
|
|
65
|
+
}
|
|
66
|
+
// ── Validation Rendering ────────────────────────────────────────────────────
|
|
67
|
+
export function renderBadReferences(badRefs, log) {
|
|
68
|
+
log(ux.colorize('yellow', ux.colorize('bold', '=== Unresolved References ===')));
|
|
69
|
+
log('');
|
|
70
|
+
log(ux.colorize('yellow', "The following references point to objects that don't exist in this push or on the server."));
|
|
71
|
+
log(ux.colorize('yellow', 'These will become placeholder statements after import.'));
|
|
72
|
+
log('');
|
|
73
|
+
for (const ref of badRefs) {
|
|
74
|
+
log(` ${ux.colorize('yellow', 'WARNING'.padEnd(16))} ${ref.sourceType.padEnd(18)} ${ref.source}`);
|
|
75
|
+
log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', `${ref.statementType} → ${ref.targetType} "${ref.target}" does not exist`)}`);
|
|
76
|
+
}
|
|
77
|
+
log('');
|
|
78
|
+
}
|
|
79
|
+
export function renderBadIndexes(badIndexes, log) {
|
|
80
|
+
log('');
|
|
81
|
+
log(ux.colorize('red', ux.colorize('bold', '=== CRITICAL: Invalid Indexes ===')));
|
|
82
|
+
log('');
|
|
83
|
+
log(ux.colorize('red', 'The following tables have indexed referencing fields that do not exist in the schema, which may cause related issues.'));
|
|
84
|
+
log('');
|
|
85
|
+
for (const idx of badIndexes) {
|
|
86
|
+
log(` ${ux.colorize('red', 'CRITICAL'.padEnd(16))} ${'table'.padEnd(18)} ${idx.table}`);
|
|
87
|
+
log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', `${idx.indexType} index → field "${idx.field}" does not exist in schema`)}`);
|
|
88
|
+
}
|
|
89
|
+
log('');
|
|
90
|
+
}
|
|
91
|
+
// ── Preview Rendering ───────────────────────────────────────────────────────
|
|
92
|
+
const TYPE_LABELS = {
|
|
93
|
+
addon: 'Addons',
|
|
94
|
+
agent: 'Agents',
|
|
95
|
+
api_group: 'API Groups',
|
|
96
|
+
function: 'Functions',
|
|
97
|
+
mcp_server: 'MCP Servers',
|
|
98
|
+
middleware: 'Middleware',
|
|
99
|
+
query: 'API Endpoints',
|
|
100
|
+
realtime_channel: 'Realtime Channels',
|
|
101
|
+
table: 'Tables',
|
|
102
|
+
task: 'Tasks',
|
|
103
|
+
tool: 'Tools',
|
|
104
|
+
toolset: 'Toolsets',
|
|
105
|
+
trigger: 'Triggers',
|
|
106
|
+
workflow_test: 'Workflow Tests',
|
|
107
|
+
workspace: 'Workspace Settings',
|
|
108
|
+
};
|
|
109
|
+
function renderPreview(result, willDelete, target, verbose, partial, log) {
|
|
110
|
+
log('');
|
|
111
|
+
log(ux.colorize('bold', `=== Push Preview: ${target.label} ===`));
|
|
112
|
+
let instanceHost = target.instanceOrigin;
|
|
113
|
+
try {
|
|
114
|
+
instanceHost = new URL(target.instanceOrigin).hostname;
|
|
115
|
+
}
|
|
116
|
+
catch { }
|
|
117
|
+
const contextParts = [
|
|
118
|
+
`instance: ${instanceHost}`,
|
|
119
|
+
];
|
|
120
|
+
if (result.workspace_name && target.supportsBranches) {
|
|
121
|
+
contextParts.push(`workspace: ${result.workspace_name}`);
|
|
122
|
+
}
|
|
123
|
+
contextParts.push(`cli: v${target.cliVersion}`);
|
|
124
|
+
log(ux.colorize('dim', ` ${contextParts.join(' | ')}`));
|
|
125
|
+
if (!partial) {
|
|
126
|
+
log(ux.colorize('red', ' --sync: all documents will be sent, including unchanged'));
|
|
127
|
+
}
|
|
128
|
+
log('');
|
|
129
|
+
for (const [type, counts] of Object.entries(result.summary)) {
|
|
130
|
+
const label = TYPE_LABELS[type] || type;
|
|
131
|
+
const parts = [];
|
|
132
|
+
if (counts.created > 0) {
|
|
133
|
+
parts.push(ux.colorize('green', `+${counts.created} created`));
|
|
134
|
+
}
|
|
135
|
+
if (counts.updated > 0) {
|
|
136
|
+
parts.push(ux.colorize('yellow', `~${counts.updated} updated`));
|
|
137
|
+
}
|
|
138
|
+
if (willDelete && counts.deleted > 0) {
|
|
139
|
+
parts.push(ux.colorize('red', `-${counts.deleted} deleted`));
|
|
140
|
+
}
|
|
141
|
+
if (counts.truncated > 0) {
|
|
142
|
+
parts.push(ux.colorize('yellow', `${counts.truncated} truncated`));
|
|
143
|
+
}
|
|
144
|
+
if (parts.length > 0) {
|
|
145
|
+
log(` ${label.padEnd(20)} ${parts.join(' ')}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const changes = result.operations.filter((op) => op.action === 'create' || op.action === 'update' || op.action === 'add_field' || op.action === 'update_field');
|
|
149
|
+
const destructive = result.operations.filter((op) => op.action === 'delete' ||
|
|
150
|
+
op.action === 'cascade_delete' ||
|
|
151
|
+
op.action === 'truncate' ||
|
|
152
|
+
op.action === 'drop_field' ||
|
|
153
|
+
op.action === 'alter_field');
|
|
154
|
+
if (changes.length > 0) {
|
|
155
|
+
log('');
|
|
156
|
+
log(ux.colorize('bold', '--- Changes ---'));
|
|
157
|
+
log('');
|
|
158
|
+
for (const op of changes) {
|
|
159
|
+
const color = op.action === 'update' || op.action === 'update_field' ? 'yellow' : 'green';
|
|
160
|
+
const actionLabel = op.action.toUpperCase();
|
|
161
|
+
log(` ${ux.colorize(color, actionLabel.padEnd(16))} ${op.type.padEnd(18)} ${op.name}`);
|
|
162
|
+
if (verbose && op.details) {
|
|
163
|
+
log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', op.details)}`);
|
|
164
|
+
}
|
|
165
|
+
if (verbose && op.reason) {
|
|
166
|
+
log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', `reason: ${op.reason}`)}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Split destructive ops by category
|
|
171
|
+
const deleteOps = destructive.filter((op) => op.action === 'delete' || op.action === 'cascade_delete');
|
|
172
|
+
const alwaysDestructive = destructive.filter((op) => op.action === 'truncate' || op.action === 'drop_field' || op.action === 'alter_field');
|
|
173
|
+
// Show destructive operations (deletes only when --delete, truncates/drop_field always)
|
|
174
|
+
const shownDestructive = [...(willDelete ? deleteOps : []), ...alwaysDestructive];
|
|
175
|
+
if (shownDestructive.length > 0) {
|
|
176
|
+
log('');
|
|
177
|
+
log(ux.colorize('bold', '--- Destructive Operations ---'));
|
|
178
|
+
log('');
|
|
179
|
+
for (const op of shownDestructive) {
|
|
180
|
+
const color = op.action === 'truncate' || op.action === 'alter_field' ? 'yellow' : 'red';
|
|
181
|
+
const actionLabel = op.action.toUpperCase();
|
|
182
|
+
log(` ${ux.colorize(color, actionLabel.padEnd(16))} ${op.type.padEnd(18)} ${op.name}`);
|
|
183
|
+
if (verbose && op.details) {
|
|
184
|
+
log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', op.details)}`);
|
|
185
|
+
}
|
|
186
|
+
if (verbose && op.reason) {
|
|
187
|
+
log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', `reason: ${op.reason}`)}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Warn about potential field renames (add + drop on same table)
|
|
192
|
+
const addFieldTables = new Set(result.operations.filter((op) => op.action === 'add_field').map((op) => op.name));
|
|
193
|
+
const dropFieldTables = new Set(result.operations.filter((op) => op.action === 'drop_field').map((op) => op.name));
|
|
194
|
+
const renameCandidates = [...addFieldTables].filter((t) => dropFieldTables.has(t));
|
|
195
|
+
if (renameCandidates.length > 0) {
|
|
196
|
+
log('');
|
|
197
|
+
log(ux.colorize('yellow', ` Note: Table(s) ${renameCandidates.map((t) => `"${t}"`).join(', ')} have both added and dropped fields.`));
|
|
198
|
+
log(ux.colorize('yellow', ' If this is intended to be a field rename, use the Xano Admin — renaming is not'));
|
|
199
|
+
log(ux.colorize('yellow', ' currently available through the CLI or Metadata API.'));
|
|
200
|
+
}
|
|
201
|
+
// Show remote-only items when not using --delete (skip for partial pushes)
|
|
202
|
+
if (!willDelete && !partial && deleteOps.length > 0) {
|
|
203
|
+
log('');
|
|
204
|
+
log(ux.colorize('dim', '--- Remote Only (not included in push) ---'));
|
|
205
|
+
log('');
|
|
206
|
+
for (const op of deleteOps) {
|
|
207
|
+
log(ux.colorize('dim', ` ${op.type.padEnd(18)} ${op.name}`));
|
|
208
|
+
}
|
|
209
|
+
log('');
|
|
210
|
+
log(ux.colorize('dim', ` Use --delete to remove these ${deleteOps.length} item(s) from remote.`));
|
|
211
|
+
}
|
|
212
|
+
log('');
|
|
213
|
+
}
|
|
214
|
+
// ── Confirmation ────────────────────────────────────────────────────────────
|
|
215
|
+
export async function confirm(message) {
|
|
216
|
+
const readline = await import('node:readline');
|
|
217
|
+
const rl = readline.createInterface({
|
|
218
|
+
input: process.stdin,
|
|
219
|
+
output: process.stdout,
|
|
220
|
+
});
|
|
221
|
+
return new Promise((resolve) => {
|
|
222
|
+
let answered = false;
|
|
223
|
+
rl.on('close', () => {
|
|
224
|
+
if (!answered)
|
|
225
|
+
resolve(false);
|
|
226
|
+
});
|
|
227
|
+
rl.question(`${message} (y/N) `, (answer) => {
|
|
228
|
+
answered = true;
|
|
229
|
+
rl.close();
|
|
230
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
// ── GUID Sync ───────────────────────────────────────────────────────────────
|
|
235
|
+
const GUID_REGEX = /guid\s*=\s*(["'])([^"']*)\1/;
|
|
236
|
+
/**
|
|
237
|
+
* Sync a GUID into a local .xs file. Returns true if the file was modified.
|
|
238
|
+
*/
|
|
239
|
+
function syncGuidToFile(filePath, guid) {
|
|
240
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
241
|
+
const existingMatch = content.match(GUID_REGEX);
|
|
242
|
+
if (existingMatch) {
|
|
243
|
+
if (existingMatch[2] === guid) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
const updated = content.replace(GUID_REGEX, `guid = "${guid}"`);
|
|
247
|
+
fs.writeFileSync(filePath, updated, 'utf8');
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
// No GUID line exists — insert before the final closing brace
|
|
251
|
+
const lines = content.split('\n');
|
|
252
|
+
let insertIndex = -1;
|
|
253
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
254
|
+
if (lines[i].trim() === '}') {
|
|
255
|
+
insertIndex = i;
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (insertIndex === -1) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
let indent = ' ';
|
|
263
|
+
for (let i = insertIndex - 1; i >= 0; i--) {
|
|
264
|
+
if (lines[i].trim()) {
|
|
265
|
+
const indentMatch = lines[i].match(/^(\s+)/);
|
|
266
|
+
if (indentMatch) {
|
|
267
|
+
indent = indentMatch[1];
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
lines.splice(insertIndex, 0, `${indent}guid = "${guid}"`);
|
|
273
|
+
fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
// ── Main Push Logic ─────────────────────────────────────────────────────────
|
|
277
|
+
/**
|
|
278
|
+
* Execute a multidoc push with preview, validation, partial mode, and GUID sync.
|
|
279
|
+
* Shared by both sandbox:push and workspace:push commands.
|
|
280
|
+
*/
|
|
281
|
+
export async function executePush(ctx, target, flags) {
|
|
282
|
+
const { accessToken, command, inputDir, verboseFetch } = ctx;
|
|
283
|
+
const log = command.log.bind(command);
|
|
284
|
+
// ── Collect and filter files ──────────────────────────────────────────
|
|
285
|
+
const allFiles = collectFiles(inputDir);
|
|
286
|
+
const files = applyFilters(allFiles, inputDir, flags.include, flags.exclude, log);
|
|
287
|
+
if (files.length === 0) {
|
|
288
|
+
command.error(flags.include || flags.exclude
|
|
289
|
+
? `No .xs files remain after ${[flags.include ? `include ${flags.include.join(', ')}` : '', flags.exclude ? `exclude ${flags.exclude.join(', ')}` : ''].filter(Boolean).join(' and ')} in ${inputDir}`
|
|
290
|
+
: `No .xs files found in ${inputDir}`);
|
|
291
|
+
}
|
|
292
|
+
// ── Read documents ────────────────────────────────────────────────────
|
|
293
|
+
const documentEntries = readDocuments(files);
|
|
294
|
+
if (documentEntries.length === 0) {
|
|
295
|
+
command.error(`All .xs files in ${inputDir} are empty`);
|
|
296
|
+
}
|
|
297
|
+
let multidoc = documentEntries.map((d) => d.content).join('\n---\n');
|
|
298
|
+
// ── Build document key → file path map (for GUID writeback) ───────────
|
|
299
|
+
const documentFileMap = new Map();
|
|
300
|
+
for (const entry of documentEntries) {
|
|
301
|
+
const parsed = parseDocument(entry.content);
|
|
302
|
+
if (parsed) {
|
|
303
|
+
const key = buildDocumentKey(parsed.type, parsed.name, parsed.verb, parsed.apiGroup);
|
|
304
|
+
documentFileMap.set(key, entry.filePath);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// ── Resolve push mode ─────────────────────────────────────────────────
|
|
308
|
+
const isPartial = !flags.sync;
|
|
309
|
+
if (flags.delete && isPartial) {
|
|
310
|
+
command.error('Cannot use --delete without --sync');
|
|
311
|
+
}
|
|
312
|
+
const shouldDelete = isPartial ? false : flags.delete;
|
|
313
|
+
// ── Build query params ────────────────────────────────────────────────
|
|
314
|
+
const queryParams = new URLSearchParams({
|
|
315
|
+
delete: shouldDelete.toString(),
|
|
316
|
+
env: flags.env.toString(),
|
|
317
|
+
records: flags.records.toString(),
|
|
318
|
+
transaction: flags.transaction.toString(),
|
|
319
|
+
truncate: flags.truncate.toString(),
|
|
320
|
+
});
|
|
321
|
+
if (target.supportsBranches && ctx.branch) {
|
|
322
|
+
queryParams.set('branch', ctx.branch);
|
|
323
|
+
}
|
|
324
|
+
if (target.supportsPartial) {
|
|
325
|
+
queryParams.set('partial', isPartial.toString());
|
|
326
|
+
}
|
|
327
|
+
// ── Request headers ───────────────────────────────────────────────────
|
|
328
|
+
const requestHeaders = {
|
|
329
|
+
accept: 'application/json',
|
|
330
|
+
Authorization: `Bearer ${accessToken}`,
|
|
331
|
+
'Content-Type': 'text/x-xanoscript',
|
|
332
|
+
};
|
|
333
|
+
// ── Dry-run / Preview ─────────────────────────────────────────────────
|
|
334
|
+
let dryRunPreview = null;
|
|
335
|
+
const dryRunUrl = target.buildDryRunUrl(queryParams);
|
|
336
|
+
if (dryRunUrl && (flags['dry-run'] || !flags.force)) {
|
|
337
|
+
const dryRunParams = new URLSearchParams(queryParams);
|
|
338
|
+
// Always request delete info in dry-run to show remote-only items
|
|
339
|
+
// and to know what exists on the server for reference checking
|
|
340
|
+
dryRunParams.set('delete', 'true');
|
|
341
|
+
const fullDryRunUrl = target.buildDryRunUrl(dryRunParams);
|
|
342
|
+
try {
|
|
343
|
+
const dryRunResponse = await verboseFetch(fullDryRunUrl, {
|
|
344
|
+
body: multidoc,
|
|
345
|
+
headers: requestHeaders,
|
|
346
|
+
method: 'POST',
|
|
347
|
+
}, flags.verbose, accessToken);
|
|
348
|
+
if (!dryRunResponse.ok) {
|
|
349
|
+
await handleDryRunError(dryRunResponse, command, flags, target);
|
|
350
|
+
// If we get here, the user confirmed to proceed without preview
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
const dryRunText = await dryRunResponse.text();
|
|
354
|
+
const preview = JSON.parse(dryRunText);
|
|
355
|
+
dryRunPreview = preview;
|
|
356
|
+
if (preview && preview.summary) {
|
|
357
|
+
renderPreview(preview, shouldDelete, target, flags.verbose, isPartial, log);
|
|
358
|
+
// Check for bad cross-references using dry-run operations to avoid false positives
|
|
359
|
+
const badRefs = checkReferences(documentEntries, preview.operations);
|
|
360
|
+
if (badRefs.length > 0) {
|
|
361
|
+
renderBadReferences(badRefs, log);
|
|
362
|
+
}
|
|
363
|
+
// Check for indexes referencing non-existent schema fields
|
|
364
|
+
const badIndexes = checkTableIndexes(documentEntries);
|
|
365
|
+
if (badIndexes.length > 0) {
|
|
366
|
+
renderBadIndexes(badIndexes, log);
|
|
367
|
+
}
|
|
368
|
+
// Check for critical errors that must block the push
|
|
369
|
+
const criticalOps = preview.operations.filter((op) => op.details?.includes('exception:') || op.details?.includes('mvp:placeholder'));
|
|
370
|
+
if (criticalOps.length > 0) {
|
|
371
|
+
log('');
|
|
372
|
+
log(ux.colorize('red', ux.colorize('bold', '=== CRITICAL ERRORS ===')));
|
|
373
|
+
log('');
|
|
374
|
+
log(ux.colorize('red', 'The following items contain syntax errors or unresolved placeholder statements'));
|
|
375
|
+
log(ux.colorize('red', 'that would corrupt data if pushed. These must be resolved first:'));
|
|
376
|
+
log('');
|
|
377
|
+
for (const op of criticalOps) {
|
|
378
|
+
log(` ${ux.colorize('red', 'BLOCKED'.padEnd(16))} ${op.type.padEnd(18)} ${op.name}`);
|
|
379
|
+
if (op.details) {
|
|
380
|
+
log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', op.details)}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
log('');
|
|
384
|
+
log(ux.colorize('red', `Push blocked: ${criticalOps.length} critical error(s) found.`));
|
|
385
|
+
if (!flags.force) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
log(ux.colorize('yellow', 'Proceeding anyway due to --force flag.'));
|
|
389
|
+
}
|
|
390
|
+
// Check for actual changes
|
|
391
|
+
const hasChanges = Object.values(preview.summary).some((c) => c.created > 0 || c.updated > 0 || (shouldDelete && c.deleted > 0) || c.truncated > 0);
|
|
392
|
+
// Detect local records
|
|
393
|
+
const tablesWithRecords = flags.records
|
|
394
|
+
? documentEntries
|
|
395
|
+
.filter((d) => /^table\s+/m.test(d.content) && /\bitems\s*=\s*\[/m.test(d.content))
|
|
396
|
+
.map((d) => {
|
|
397
|
+
const nameMatch = d.content.match(/^table\s+(\S+)/m);
|
|
398
|
+
const itemsMatch = d.content.match(/\bitems\s*=\s*\[([\s\S]*?)\n\s*\]/);
|
|
399
|
+
const itemCount = itemsMatch ? (itemsMatch[1].match(/^\s*\{/gm) || []).length : 0;
|
|
400
|
+
return { name: nameMatch ? nameMatch[1] : 'unknown', records: itemCount };
|
|
401
|
+
})
|
|
402
|
+
: [];
|
|
403
|
+
const hasLocalRecords = tablesWithRecords.length > 0;
|
|
404
|
+
if (hasLocalRecords) {
|
|
405
|
+
log('');
|
|
406
|
+
log(ux.colorize('bold', '--- Records ---'));
|
|
407
|
+
log('');
|
|
408
|
+
for (const t of tablesWithRecords) {
|
|
409
|
+
log(` ${ux.colorize('yellow', 'UPSERT'.padEnd(16))} ${'table'.padEnd(18)} ${t.name} (${t.records} records)`);
|
|
410
|
+
}
|
|
411
|
+
log('');
|
|
412
|
+
}
|
|
413
|
+
if (!hasChanges && !hasLocalRecords) {
|
|
414
|
+
log('');
|
|
415
|
+
log('No changes to push.');
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (flags['dry-run']) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
// Confirm with user
|
|
422
|
+
const hasDestructive = preview.operations.some((op) => (shouldDelete && (op.action === 'delete' || op.action === 'cascade_delete')) ||
|
|
423
|
+
op.action === 'truncate' ||
|
|
424
|
+
op.action === 'drop_field' ||
|
|
425
|
+
op.action === 'alter_field');
|
|
426
|
+
const message = hasDestructive
|
|
427
|
+
? 'Proceed with push? This includes DESTRUCTIVE operations listed above.'
|
|
428
|
+
: 'Proceed with push?';
|
|
429
|
+
if (process.stdin.isTTY) {
|
|
430
|
+
const confirmed = await confirm(message);
|
|
431
|
+
if (!confirmed) {
|
|
432
|
+
log('Push cancelled.');
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
command.error('Non-interactive environment detected. Use --force to skip confirmation.');
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
// Server returned unexpected response
|
|
442
|
+
log('');
|
|
443
|
+
log(ux.colorize('dim', 'Push preview not yet available on this instance.'));
|
|
444
|
+
log('');
|
|
445
|
+
await confirmOrAbort(command, log);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
// Ctrl+C or SIGINT
|
|
451
|
+
if (error.name === 'AbortError' || error.code === 'ERR_USE_AFTER_CLOSE') {
|
|
452
|
+
log('\nPush cancelled.');
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
// Re-throw oclif errors
|
|
456
|
+
if (error instanceof Error && 'oclif' in error) {
|
|
457
|
+
throw error;
|
|
458
|
+
}
|
|
459
|
+
// Dry-run failed unexpectedly — proceed without preview
|
|
460
|
+
log('');
|
|
461
|
+
log(ux.colorize('dim', 'Push preview not yet available on this instance.'));
|
|
462
|
+
if (flags.verbose) {
|
|
463
|
+
log(ux.colorize('dim', ` ${error.message}`));
|
|
464
|
+
}
|
|
465
|
+
log('');
|
|
466
|
+
await confirmOrAbort(command, log);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// ── Show bad references in force mode (preview mode shows them inline) ─
|
|
470
|
+
if (flags.force) {
|
|
471
|
+
const badRefs = checkReferences(documentEntries);
|
|
472
|
+
if (badRefs.length > 0) {
|
|
473
|
+
log('');
|
|
474
|
+
renderBadReferences(badRefs, log);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// ── Partial push: filter to changed documents only ────────────────────
|
|
478
|
+
if (isPartial && dryRunPreview) {
|
|
479
|
+
const changedKeys = new Set(dryRunPreview.operations
|
|
480
|
+
.filter((op) => op.action !== 'unchanged' && op.action !== 'delete' && op.action !== 'cascade_delete')
|
|
481
|
+
.map((op) => `${op.type}:${op.name}`));
|
|
482
|
+
const filteredEntries = documentEntries.filter((entry) => {
|
|
483
|
+
const parsed = parseDocument(entry.content);
|
|
484
|
+
if (!parsed)
|
|
485
|
+
return true;
|
|
486
|
+
// Workspace settings always use a fixed key in dry-run regardless of the actual name
|
|
487
|
+
if (parsed.type === 'workspace' && changedKeys.has('workspace:workspace'))
|
|
488
|
+
return true;
|
|
489
|
+
const opName = parsed.verb ? `${parsed.name} ${parsed.verb}` : parsed.name;
|
|
490
|
+
if (changedKeys.has(`${parsed.type}:${opName}`))
|
|
491
|
+
return true;
|
|
492
|
+
// Keep table documents that contain records when --records is active
|
|
493
|
+
if (flags.records && parsed.type === 'table' && /\bitems\s*=\s*\[/m.test(entry.content))
|
|
494
|
+
return true;
|
|
495
|
+
return false;
|
|
496
|
+
});
|
|
497
|
+
if (filteredEntries.length === 0) {
|
|
498
|
+
log('No changes to push.');
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
multidoc = filteredEntries.map((d) => d.content).join('\n---\n');
|
|
502
|
+
}
|
|
503
|
+
// ── Execute the actual push ───────────────────────────────────────────
|
|
504
|
+
const apiUrl = target.buildPushUrl(queryParams);
|
|
505
|
+
const startTime = Date.now();
|
|
506
|
+
try {
|
|
507
|
+
const response = await verboseFetch(apiUrl, {
|
|
508
|
+
body: multidoc,
|
|
509
|
+
headers: requestHeaders,
|
|
510
|
+
method: 'POST',
|
|
511
|
+
}, flags.verbose, accessToken);
|
|
512
|
+
if (!response.ok) {
|
|
513
|
+
handlePushError(response, await response.text(), documentEntries, inputDir, command);
|
|
514
|
+
}
|
|
515
|
+
// Parse response for GUID map
|
|
516
|
+
const responseText = await response.text();
|
|
517
|
+
let guidMap = [];
|
|
518
|
+
if (responseText && responseText !== 'null') {
|
|
519
|
+
try {
|
|
520
|
+
const responseJson = JSON.parse(responseText);
|
|
521
|
+
if (responseJson?.guid_map && Array.isArray(responseJson.guid_map)) {
|
|
522
|
+
guidMap = responseJson.guid_map;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
if (flags.verbose) {
|
|
527
|
+
log('Server response is not JSON; skipping GUID sync');
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
// Write GUIDs back to local files
|
|
532
|
+
if (flags.guids && guidMap.length > 0) {
|
|
533
|
+
const baseKeyMap = new Map();
|
|
534
|
+
for (const [key, fp] of documentFileMap) {
|
|
535
|
+
const baseKey = key.split(':').slice(0, 2).join(':');
|
|
536
|
+
if (baseKeyMap.has(baseKey)) {
|
|
537
|
+
baseKeyMap.set(baseKey, ''); // Mark as ambiguous
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
baseKeyMap.set(baseKey, fp);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
let updatedCount = 0;
|
|
544
|
+
for (const entry of guidMap) {
|
|
545
|
+
if (!entry.guid)
|
|
546
|
+
continue;
|
|
547
|
+
const key = buildDocumentKey(entry.type, entry.name, entry.verb, entry.api_group);
|
|
548
|
+
let filePath = documentFileMap.get(key);
|
|
549
|
+
if (!filePath) {
|
|
550
|
+
const baseKey = `${entry.type}:${entry.name}`;
|
|
551
|
+
const basePath = baseKeyMap.get(baseKey);
|
|
552
|
+
if (basePath) {
|
|
553
|
+
filePath = basePath;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (!filePath) {
|
|
557
|
+
if (flags.verbose) {
|
|
558
|
+
log(` No local file found for ${entry.type} "${entry.name}", skipping GUID sync`);
|
|
559
|
+
}
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
try {
|
|
563
|
+
const updated = syncGuidToFile(filePath, entry.guid);
|
|
564
|
+
if (updated)
|
|
565
|
+
updatedCount++;
|
|
566
|
+
}
|
|
567
|
+
catch (error) {
|
|
568
|
+
command.warn(`Failed to sync GUID to ${filePath}: ${error.message}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (updatedCount > 0) {
|
|
572
|
+
log(`Synced ${updatedCount} GUIDs to local files`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
if (error instanceof Error && 'oclif' in error)
|
|
578
|
+
throw error;
|
|
579
|
+
if (error instanceof Error) {
|
|
580
|
+
command.error(`Failed to push multidoc: ${error.message}`);
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
command.error(`Failed to push multidoc: ${String(error)}`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
587
|
+
const pushedCount = multidoc.split('\n---\n').length;
|
|
588
|
+
log(`Pushed ${pushedCount} documents to ${target.label} from ${relative(process.cwd(), inputDir) || inputDir} in ${elapsed}s`);
|
|
589
|
+
}
|
|
590
|
+
// ── Error Handlers ──────────────────────────────────────────────────────────
|
|
591
|
+
async function handleDryRunError(response, command, flags, target) {
|
|
592
|
+
const log = command.log.bind(command);
|
|
593
|
+
if (response.status === 404) {
|
|
594
|
+
const errorText = await response.text();
|
|
595
|
+
try {
|
|
596
|
+
const errorJson = JSON.parse(errorText);
|
|
597
|
+
if (errorJson.message) {
|
|
598
|
+
command.error(errorJson.message);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
// Not JSON
|
|
603
|
+
}
|
|
604
|
+
if (target.supportsBranches) {
|
|
605
|
+
command.error('Workspace not found. Check the workspace ID and try again.');
|
|
606
|
+
}
|
|
607
|
+
log('');
|
|
608
|
+
log(ux.colorize('dim', 'Push preview not yet available on this instance.'));
|
|
609
|
+
log('');
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
const errorText = await response.text();
|
|
613
|
+
// Check if push is disabled
|
|
614
|
+
try {
|
|
615
|
+
const errorJson = JSON.parse(errorText);
|
|
616
|
+
if (errorJson.message?.includes('Push is disabled')) {
|
|
617
|
+
log('');
|
|
618
|
+
log(ux.colorize('red', ux.colorize('bold', 'Direct push is disabled to protect your production workspace from unintended changes.')));
|
|
619
|
+
log(ux.colorize('dim', 'Use your sandbox environment to test and review changes before applying them to your production workspace.'));
|
|
620
|
+
log('');
|
|
621
|
+
log(ux.colorize('dim', 'To apply changes to the workspace, use the sandbox review flow:'));
|
|
622
|
+
log(` ${ux.colorize('cyan', 'xano sandbox push')} ${ux.colorize('dim', '— push changes to your sandbox')}`);
|
|
623
|
+
log(` ${ux.colorize('cyan', 'xano sandbox review')} ${ux.colorize('dim', '— edit any logic, inspect the snapshot diff, and promote changes to the workspace')}`);
|
|
624
|
+
log('');
|
|
625
|
+
log(ux.colorize('dim', 'To enable direct push, go to Workspace Settings → CLI → Allow Direct Workspace Push.'));
|
|
626
|
+
log('');
|
|
627
|
+
log(ux.colorize('dim', "Note: Free plan instances don't include sandbox environments, so direct push is always enabled."));
|
|
628
|
+
log('');
|
|
629
|
+
process.exit(0);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
catch {
|
|
633
|
+
// Not JSON, fall through
|
|
634
|
+
}
|
|
635
|
+
command.warn(`Push preview failed (${response.status}). Skipping preview.`);
|
|
636
|
+
if (flags.verbose) {
|
|
637
|
+
log(ux.colorize('dim', errorText));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
await confirmOrAbort(command, log);
|
|
641
|
+
}
|
|
642
|
+
async function confirmOrAbort(command, log) {
|
|
643
|
+
if (process.stdin.isTTY) {
|
|
644
|
+
const confirmed = await confirm('Proceed with push?');
|
|
645
|
+
if (!confirmed) {
|
|
646
|
+
log('Push cancelled.');
|
|
647
|
+
command.exit(0);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
command.error('Non-interactive environment detected. Use --force to skip confirmation.');
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
function handlePushError(response, errorText, documentEntries, inputDir, command) {
|
|
655
|
+
let errorMessage = `Push failed (${response.status})`;
|
|
656
|
+
try {
|
|
657
|
+
const errorJson = JSON.parse(errorText);
|
|
658
|
+
errorMessage += `: ${errorJson.message}`;
|
|
659
|
+
if (errorJson.payload?.param) {
|
|
660
|
+
errorMessage += `\n Parameter: ${errorJson.payload.param}`;
|
|
661
|
+
}
|
|
662
|
+
// Provide guidance when push is disabled (workspace-specific)
|
|
663
|
+
if (errorJson.message?.includes('Push is disabled')) {
|
|
664
|
+
command.error(`Direct push is disabled to protect your production workspace from unintended changes.\n` +
|
|
665
|
+
`Use your sandbox environment to test and review changes before applying them to your production workspace.\n\n` +
|
|
666
|
+
`Alternatively, use sandbox commands:\n` +
|
|
667
|
+
` xano sandbox push <directory>\n` +
|
|
668
|
+
` xano sandbox review\n\n` +
|
|
669
|
+
`To enable direct push, go to Workspace Settings → CLI → Allow Direct Workspace Push.\n\n` +
|
|
670
|
+
`Note: Free plan instances don't include sandbox environments, so direct push is always enabled.`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
catch {
|
|
674
|
+
errorMessage += `\n${errorText}`;
|
|
675
|
+
}
|
|
676
|
+
// Provide guidance when sandbox access is denied (free plan restriction)
|
|
677
|
+
if (response.status === 500 && errorMessage.includes('Access Denied')) {
|
|
678
|
+
command.error('Sandbox is not available on the Free plan. Upgrade your plan to use sandbox features.');
|
|
679
|
+
}
|
|
680
|
+
// Surface local files involved in duplicate GUID errors
|
|
681
|
+
const guidMatch = errorMessage.match(/Duplicate \w+ guid: (\S+)/);
|
|
682
|
+
if (guidMatch) {
|
|
683
|
+
const dupeFiles = findFilesWithGuid(documentEntries, guidMatch[1]);
|
|
684
|
+
if (dupeFiles.length > 0) {
|
|
685
|
+
const relPaths = dupeFiles.map((f) => relative(inputDir, f));
|
|
686
|
+
errorMessage += `\n Local files with this GUID:\n${relPaths.map((f) => ` ${f}`).join('\n')}`;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
command.error(errorMessage);
|
|
690
|
+
}
|