@xano/cli 0.0.95-beta.2 → 0.0.95-beta.21
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 +22 -12
- package/dist/base-command.d.ts +30 -0
- package/dist/base-command.js +61 -0
- package/dist/commands/auth/index.js +1 -1
- package/dist/commands/branch/create/index.d.ts +3 -1
- package/dist/commands/branch/create/index.js +21 -17
- package/dist/commands/profile/create/index.js +2 -2
- package/dist/commands/profile/edit/index.js +2 -2
- package/dist/commands/profile/me/index.js +21 -2
- package/dist/commands/profile/wizard/index.js +3 -3
- package/dist/commands/profile/workspace/set/index.js +1 -1
- package/dist/commands/{ephemeral → sandbox}/delete/index.d.ts +1 -5
- package/dist/commands/sandbox/delete/index.js +71 -0
- package/dist/commands/{ephemeral → sandbox}/env/delete/index.d.ts +1 -4
- package/dist/commands/{ephemeral → sandbox}/env/delete/index.js +20 -36
- package/dist/commands/{ephemeral → sandbox}/env/get/index.d.ts +1 -4
- package/dist/commands/sandbox/env/get/index.js +65 -0
- package/dist/commands/{ephemeral → sandbox}/env/get_all/index.d.ts +1 -4
- package/dist/commands/sandbox/env/get_all/index.js +78 -0
- package/dist/commands/{ephemeral → sandbox}/env/list/index.d.ts +1 -4
- package/dist/commands/sandbox/env/list/index.js +67 -0
- package/dist/commands/{ephemeral → sandbox}/env/set/index.d.ts +1 -4
- package/dist/commands/sandbox/env/set/index.js +74 -0
- package/dist/commands/{ephemeral → sandbox}/env/set_all/index.d.ts +1 -4
- package/dist/commands/{ephemeral → sandbox}/env/set_all/index.js +19 -35
- package/dist/commands/{ephemeral → sandbox}/get/index.d.ts +1 -4
- 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/{ephemeral → sandbox}/license/get/index.d.ts +1 -4
- package/dist/commands/sandbox/license/get/index.js +78 -0
- package/dist/commands/{ephemeral → sandbox}/license/set/index.d.ts +1 -4
- package/dist/commands/{ephemeral → sandbox}/license/set/index.js +20 -36
- package/dist/commands/{ephemeral → sandbox}/pull/index.d.ts +1 -2
- package/dist/commands/{ephemeral → sandbox}/pull/index.js +13 -28
- package/dist/commands/{ephemeral → sandbox}/push/index.d.ts +3 -2
- package/dist/commands/{ephemeral → sandbox}/push/index.js +56 -31
- package/dist/commands/sandbox/reset/index.d.ts +12 -0
- package/dist/commands/sandbox/reset/index.js +71 -0
- package/dist/commands/{ephemeral/impersonate → sandbox/review}/index.d.ts +1 -4
- package/dist/commands/{ephemeral/impersonate → sandbox/review}/index.js +17 -33
- package/dist/commands/{ephemeral/unit_test/run_all → sandbox/unit_test/list}/index.d.ts +1 -2
- package/dist/commands/{ephemeral → sandbox}/unit_test/list/index.js +12 -26
- package/dist/commands/{ephemeral → sandbox}/unit_test/run/index.d.ts +1 -2
- package/dist/commands/{ephemeral → sandbox}/unit_test/run/index.js +11 -25
- package/dist/commands/{ephemeral/unit_test/list → sandbox/unit_test/run_all}/index.d.ts +1 -2
- package/dist/commands/{ephemeral → sandbox}/unit_test/run_all/index.js +11 -23
- package/dist/commands/{ephemeral/workflow_test/run_all → sandbox/workflow_test/list}/index.d.ts +1 -2
- package/dist/commands/{ephemeral → sandbox}/workflow_test/list/index.js +13 -27
- package/dist/commands/{ephemeral/workflow_test/get → sandbox/workflow_test/run}/index.d.ts +1 -2
- package/dist/commands/{ephemeral → sandbox}/workflow_test/run/index.js +11 -25
- package/dist/commands/{ephemeral/workflow_test/list → sandbox/workflow_test/run_all}/index.d.ts +1 -2
- package/dist/commands/{ephemeral → sandbox}/workflow_test/run_all/index.js +11 -23
- package/dist/commands/tenant/create/index.d.ts +2 -1
- package/dist/commands/tenant/create/index.js +23 -6
- package/dist/commands/tenant/deploy_release/index.d.ts +1 -0
- package/dist/commands/tenant/deploy_release/index.js +9 -1
- package/dist/commands/tenant/get/index.js +2 -2
- package/dist/commands/tenant/list/index.js +2 -2
- 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.js +2 -27
- package/dist/commands/tenant/unit_test/run/index.js +2 -27
- package/dist/commands/tenant/unit_test/run_all/index.js +2 -27
- package/dist/commands/tenant/workflow_test/list/index.js +2 -27
- package/dist/commands/tenant/workflow_test/run/index.js +2 -27
- package/dist/commands/tenant/workflow_test/run_all/index.js +2 -27
- package/dist/commands/workspace/edit/index.d.ts +1 -0
- package/dist/commands/workspace/edit/index.js +16 -6
- package/dist/commands/workspace/get/index.js +9 -7
- package/dist/commands/workspace/list/index.d.ts +1 -0
- package/dist/commands/workspace/list/index.js +14 -7
- package/dist/commands/workspace/push/index.d.ts +2 -0
- package/dist/commands/workspace/push/index.js +81 -6
- package/dist/utils/reference-checker.d.ts +57 -0
- package/dist/utils/reference-checker.js +232 -0
- package/oclif.manifest.json +1934 -2720
- package/package.json +8 -8
- package/dist/commands/ephemeral/access/index.d.ts +0 -15
- package/dist/commands/ephemeral/access/index.js +0 -78
- package/dist/commands/ephemeral/create/index.d.ts +0 -17
- package/dist/commands/ephemeral/create/index.js +0 -102
- package/dist/commands/ephemeral/delete/index.js +0 -99
- package/dist/commands/ephemeral/env/get/index.js +0 -81
- package/dist/commands/ephemeral/env/get_all/index.js +0 -94
- package/dist/commands/ephemeral/env/list/index.js +0 -83
- package/dist/commands/ephemeral/env/set/index.js +0 -90
- package/dist/commands/ephemeral/get/index.js +0 -102
- package/dist/commands/ephemeral/license/get/index.js +0 -94
- package/dist/commands/ephemeral/list/index.d.ts +0 -15
- package/dist/commands/ephemeral/list/index.js +0 -109
- package/dist/commands/ephemeral/shared/index.d.ts +0 -15
- package/dist/commands/ephemeral/shared/index.js +0 -108
- package/dist/commands/ephemeral/workflow_test/delete/index.d.ts +0 -18
- package/dist/commands/ephemeral/workflow_test/delete/index.js +0 -75
- package/dist/commands/ephemeral/workflow_test/get/index.js +0 -77
- package/dist/commands/ephemeral/workflow_test/run/index.d.ts +0 -18
- package/dist/commands/tenant/workflow_test/delete/index.d.ts +0 -19
- package/dist/commands/tenant/workflow_test/delete/index.js +0 -110
- package/dist/commands/tenant/workflow_test/get/index.d.ts +0 -19
- package/dist/commands/tenant/workflow_test/get/index.js +0 -112
|
@@ -35,6 +35,11 @@ Updated workspace: my-workspace (ID: 123)
|
|
|
35
35
|
];
|
|
36
36
|
static flags = {
|
|
37
37
|
...BaseCommand.baseFlags,
|
|
38
|
+
'allow-push': Flags.boolean({
|
|
39
|
+
allowNo: true,
|
|
40
|
+
description: 'Enable or disable direct CLI push to this workspace (not applicable on Free plan)',
|
|
41
|
+
required: false,
|
|
42
|
+
}),
|
|
38
43
|
description: Flags.string({
|
|
39
44
|
char: 'd',
|
|
40
45
|
description: 'New description for the workspace',
|
|
@@ -102,9 +107,12 @@ Updated workspace: my-workspace (ID: 123)
|
|
|
102
107
|
if (flags['require-token'] !== undefined) {
|
|
103
108
|
body.documentation = { require_token: flags['require-token'] };
|
|
104
109
|
}
|
|
110
|
+
if (flags['allow-push'] !== undefined) {
|
|
111
|
+
body.preferences = { allow_push: flags['allow-push'] };
|
|
112
|
+
}
|
|
105
113
|
// Check if at least one field is being updated
|
|
106
114
|
if (Object.keys(body).length === 0) {
|
|
107
|
-
this.error('No fields specified to update. Use --name, --description, --swagger,
|
|
115
|
+
this.error('No fields specified to update. Use --name, --description, --swagger, --require-token, or --allow-push flags.\n' +
|
|
108
116
|
'Example: xano workspace edit 123 --name "new-name"');
|
|
109
117
|
}
|
|
110
118
|
// Construct the API URL
|
|
@@ -114,8 +122,8 @@ Updated workspace: my-workspace (ID: 123)
|
|
|
114
122
|
const response = await this.verboseFetch(apiUrl, {
|
|
115
123
|
body: JSON.stringify(body),
|
|
116
124
|
headers: {
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
accept: 'application/json',
|
|
126
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
119
127
|
'Content-Type': 'application/json',
|
|
120
128
|
},
|
|
121
129
|
method: 'PUT',
|
|
@@ -124,7 +132,7 @@ Updated workspace: my-workspace (ID: 123)
|
|
|
124
132
|
const errorText = await response.text();
|
|
125
133
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
126
134
|
}
|
|
127
|
-
const workspace = await response.json();
|
|
135
|
+
const workspace = (await response.json());
|
|
128
136
|
// Output results
|
|
129
137
|
if (flags.output === 'json') {
|
|
130
138
|
this.log(JSON.stringify(workspace, null, 2));
|
|
@@ -141,6 +149,9 @@ Updated workspace: my-workspace (ID: 123)
|
|
|
141
149
|
if (workspace.documentation?.require_token !== undefined) {
|
|
142
150
|
this.log(` Require Token: ${workspace.documentation.require_token}`);
|
|
143
151
|
}
|
|
152
|
+
if (workspace.preferences?.allow_push !== undefined) {
|
|
153
|
+
this.log(` Allow Push: ${workspace.preferences.allow_push}`);
|
|
154
|
+
}
|
|
144
155
|
}
|
|
145
156
|
}
|
|
146
157
|
catch (error) {
|
|
@@ -157,8 +168,7 @@ Updated workspace: my-workspace (ID: 123)
|
|
|
157
168
|
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
158
169
|
// Check if credentials file exists
|
|
159
170
|
if (!fs.existsSync(credentialsPath)) {
|
|
160
|
-
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
161
|
-
`Create a profile using 'xano profile create'`);
|
|
171
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` + `Create a profile using 'xano profile create'`);
|
|
162
172
|
}
|
|
163
173
|
// Read credentials file
|
|
164
174
|
try {
|
|
@@ -73,8 +73,8 @@ Workspace: my-workspace (ID: 123)
|
|
|
73
73
|
try {
|
|
74
74
|
const response = await this.verboseFetch(apiUrl, {
|
|
75
75
|
headers: {
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
accept: 'application/json',
|
|
77
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
78
78
|
},
|
|
79
79
|
method: 'GET',
|
|
80
80
|
}, flags.verbose, profile.access_token);
|
|
@@ -82,7 +82,7 @@ Workspace: my-workspace (ID: 123)
|
|
|
82
82
|
const errorText = await response.text();
|
|
83
83
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
84
84
|
}
|
|
85
|
-
const workspace = await response.json();
|
|
85
|
+
const workspace = (await response.json());
|
|
86
86
|
// Output results
|
|
87
87
|
if (flags.output === 'json') {
|
|
88
88
|
this.log(JSON.stringify(workspace, null, 2));
|
|
@@ -94,13 +94,16 @@ Workspace: my-workspace (ID: 123)
|
|
|
94
94
|
this.log(` Description: ${workspace.description}`);
|
|
95
95
|
}
|
|
96
96
|
if (workspace.created_at) {
|
|
97
|
-
const createdDate = new Date(workspace.created_at
|
|
97
|
+
const createdDate = new Date(workspace.created_at).toISOString().split('T')[0];
|
|
98
98
|
this.log(` Created: ${createdDate}`);
|
|
99
99
|
}
|
|
100
100
|
if (workspace.updated_at) {
|
|
101
|
-
const updatedDate = new Date(workspace.updated_at
|
|
101
|
+
const updatedDate = new Date(workspace.updated_at).toISOString().split('T')[0];
|
|
102
102
|
this.log(` Updated: ${updatedDate}`);
|
|
103
103
|
}
|
|
104
|
+
if (workspace.preferences?.allow_push !== undefined) {
|
|
105
|
+
this.log(` Allow Push: ${workspace.preferences.allow_push}`);
|
|
106
|
+
}
|
|
104
107
|
}
|
|
105
108
|
}
|
|
106
109
|
catch (error) {
|
|
@@ -117,8 +120,7 @@ Workspace: my-workspace (ID: 123)
|
|
|
117
120
|
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
118
121
|
// Check if credentials file exists
|
|
119
122
|
if (!fs.existsSync(credentialsPath)) {
|
|
120
|
-
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
121
|
-
`Create a profile using 'xano profile create'`);
|
|
123
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` + `Create a profile using 'xano profile create'`);
|
|
122
124
|
}
|
|
123
125
|
// Read credentials file
|
|
124
126
|
try {
|
|
@@ -3,6 +3,7 @@ export default class WorkspaceList extends BaseCommand {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
+
latest: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
6
7
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
8
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
9
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -45,6 +45,10 @@ Available workspaces:
|
|
|
45
45
|
];
|
|
46
46
|
static flags = {
|
|
47
47
|
...BaseCommand.baseFlags,
|
|
48
|
+
latest: Flags.boolean({
|
|
49
|
+
default: false,
|
|
50
|
+
description: 'Sort by newest first (descending ID)',
|
|
51
|
+
}),
|
|
48
52
|
output: Flags.string({
|
|
49
53
|
char: 'o',
|
|
50
54
|
default: 'summary',
|
|
@@ -78,8 +82,8 @@ Available workspaces:
|
|
|
78
82
|
try {
|
|
79
83
|
const response = await this.verboseFetch(apiUrl, {
|
|
80
84
|
headers: {
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
accept: 'application/json',
|
|
86
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
83
87
|
},
|
|
84
88
|
method: 'GET',
|
|
85
89
|
}, flags.verbose, profile.access_token);
|
|
@@ -87,7 +91,7 @@ Available workspaces:
|
|
|
87
91
|
const errorText = await response.text();
|
|
88
92
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
89
93
|
}
|
|
90
|
-
const data = await response.json();
|
|
94
|
+
const data = (await response.json());
|
|
91
95
|
// Handle different response formats
|
|
92
96
|
let workspaces;
|
|
93
97
|
if (Array.isArray(data)) {
|
|
@@ -99,6 +103,9 @@ Available workspaces:
|
|
|
99
103
|
else {
|
|
100
104
|
this.error('Unexpected API response format');
|
|
101
105
|
}
|
|
106
|
+
if (flags.latest) {
|
|
107
|
+
workspaces.sort((a, b) => b.id - a.id);
|
|
108
|
+
}
|
|
102
109
|
// Output results
|
|
103
110
|
if (flags.output === 'json') {
|
|
104
111
|
this.log(JSON.stringify(workspaces, null, 2));
|
|
@@ -111,11 +118,12 @@ Available workspaces:
|
|
|
111
118
|
else {
|
|
112
119
|
this.log('Available workspaces:');
|
|
113
120
|
for (const workspace of workspaces) {
|
|
121
|
+
const created = workspace.created_at ? ` (created: ${workspace.created_at.split(' ')[0]})` : '';
|
|
114
122
|
if (workspace.id === undefined) {
|
|
115
|
-
this.log(` - ${workspace.name}`);
|
|
123
|
+
this.log(` - ${workspace.name}${created}`);
|
|
116
124
|
}
|
|
117
125
|
else {
|
|
118
|
-
this.log(` - ${workspace.name} (ID: ${workspace.id})`);
|
|
126
|
+
this.log(` - ${workspace.name} (ID: ${workspace.id})${created}`);
|
|
119
127
|
}
|
|
120
128
|
}
|
|
121
129
|
}
|
|
@@ -135,8 +143,7 @@ Available workspaces:
|
|
|
135
143
|
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
136
144
|
// Check if credentials file exists
|
|
137
145
|
if (!fs.existsSync(credentialsPath)) {
|
|
138
|
-
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
139
|
-
`Create a profile using 'xano profile:create'`);
|
|
146
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` + `Create a profile using 'xano profile:create'`);
|
|
140
147
|
}
|
|
141
148
|
// Read credentials file
|
|
142
149
|
try {
|
|
@@ -6,6 +6,7 @@ import * as os from 'node:os';
|
|
|
6
6
|
import * as path from 'node:path';
|
|
7
7
|
import BaseCommand from '../../../base-command.js';
|
|
8
8
|
import { buildDocumentKey, findFilesWithGuid, parseDocument } from '../../../utils/document-parser.js';
|
|
9
|
+
import { checkReferences, checkTableIndexes } from '../../../utils/reference-checker.js';
|
|
9
10
|
export default class Push extends BaseCommand {
|
|
10
11
|
static args = {
|
|
11
12
|
directory: Args.string({
|
|
@@ -250,10 +251,10 @@ Push functions but exclude test files
|
|
|
250
251
|
let dryRunPreview = null;
|
|
251
252
|
if (flags['dry-run'] || !flags.force) {
|
|
252
253
|
const dryRunParams = new URLSearchParams(queryParams);
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
254
|
+
// Always request delete info in dry-run so we can:
|
|
255
|
+
// 1. Show remote-only items (in --sync mode)
|
|
256
|
+
// 2. Know what exists on the server (to filter unresolved reference warnings)
|
|
257
|
+
dryRunParams.set('delete', 'true');
|
|
257
258
|
const dryRunUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/multidoc/dry-run?${dryRunParams.toString()}`;
|
|
258
259
|
try {
|
|
259
260
|
const dryRunResponse = await this.verboseFetch(dryRunUrl, {
|
|
@@ -282,6 +283,28 @@ Push functions but exclude test files
|
|
|
282
283
|
}
|
|
283
284
|
else {
|
|
284
285
|
const errorText = await dryRunResponse.text();
|
|
286
|
+
// Check if push is disabled on this workspace
|
|
287
|
+
try {
|
|
288
|
+
const errorJson = JSON.parse(errorText);
|
|
289
|
+
if (errorJson.message?.includes('Push is disabled')) {
|
|
290
|
+
this.log('');
|
|
291
|
+
this.log(ux.colorize('red', ux.colorize('bold', 'Direct push is disabled to protect your production workspace from unintended changes.')));
|
|
292
|
+
this.log(ux.colorize('dim', 'Use your sandbox environment to test and review changes before applying them to your production workspace.'));
|
|
293
|
+
this.log('');
|
|
294
|
+
this.log(ux.colorize('dim', 'To apply changes to the workspace, use the sandbox review flow:'));
|
|
295
|
+
this.log(` ${ux.colorize('cyan', 'xano sandbox push')} ${ux.colorize('dim', '— push changes to your sandbox')}`);
|
|
296
|
+
this.log(` ${ux.colorize('cyan', 'xano sandbox review')} ${ux.colorize('dim', '— edit any logic, inspect the snapshot diff, and promote changes to the workspace')}`);
|
|
297
|
+
this.log('');
|
|
298
|
+
this.log(ux.colorize('dim', 'To enable direct push, go to Workspace Settings → CLI → Allow Direct Workspace Push.'));
|
|
299
|
+
this.log('');
|
|
300
|
+
this.log(ux.colorize('dim', "Note: Free plan instances don't include sandbox environments, so direct push is always enabled."));
|
|
301
|
+
this.log('');
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
// Not JSON, fall through
|
|
307
|
+
}
|
|
285
308
|
this.warn(`Push preview failed (${dryRunResponse.status}). Skipping preview.`);
|
|
286
309
|
if (flags.verbose) {
|
|
287
310
|
this.log(ux.colorize('dim', errorText));
|
|
@@ -306,6 +329,16 @@ Push functions but exclude test files
|
|
|
306
329
|
// Check if the server returned a valid dry-run response
|
|
307
330
|
if (preview && preview.summary) {
|
|
308
331
|
this.renderPreview(preview, shouldDelete, workspaceId, flags.verbose, isPartial);
|
|
332
|
+
// Check for bad cross-references, using dry-run operations to avoid false positives
|
|
333
|
+
const badRefs = checkReferences(documentEntries, preview.operations);
|
|
334
|
+
if (badRefs.length > 0) {
|
|
335
|
+
this.renderBadReferences(badRefs);
|
|
336
|
+
}
|
|
337
|
+
// Check for indexes referencing non-existent schema fields
|
|
338
|
+
const badIndexes = checkTableIndexes(documentEntries);
|
|
339
|
+
if (badIndexes.length > 0) {
|
|
340
|
+
this.renderBadIndexes(badIndexes);
|
|
341
|
+
}
|
|
309
342
|
// Check for critical errors that must block the push
|
|
310
343
|
const criticalOps = preview.operations.filter((op) => op.details?.includes('exception:') || op.details?.includes('mvp:placeholder'));
|
|
311
344
|
if (criticalOps.length > 0) {
|
|
@@ -424,6 +457,14 @@ Push functions but exclude test files
|
|
|
424
457
|
}
|
|
425
458
|
}
|
|
426
459
|
}
|
|
460
|
+
// Show bad references in force mode (preview mode shows them inline)
|
|
461
|
+
if (flags.force) {
|
|
462
|
+
const badRefs = checkReferences(documentEntries);
|
|
463
|
+
if (badRefs.length > 0) {
|
|
464
|
+
this.log('');
|
|
465
|
+
this.renderBadReferences(badRefs);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
427
468
|
// For partial pushes, filter to only changed documents
|
|
428
469
|
if (isPartial && dryRunPreview) {
|
|
429
470
|
const changedKeys = new Set(dryRunPreview.operations
|
|
@@ -465,6 +506,16 @@ Push functions but exclude test files
|
|
|
465
506
|
if (errorJson.payload?.param) {
|
|
466
507
|
errorMessage += `\n Parameter: ${errorJson.payload.param}`;
|
|
467
508
|
}
|
|
509
|
+
// Provide clear guidance when push is disabled
|
|
510
|
+
if (errorJson.message?.includes('Push is disabled')) {
|
|
511
|
+
this.error(`Direct push is disabled to protect your production workspace from unintended changes.\n` +
|
|
512
|
+
`Use your sandbox environment to test and review changes before applying them to your production workspace.\n\n` +
|
|
513
|
+
`Alternatively, use sandbox commands:\n` +
|
|
514
|
+
` xano sandbox push ${args.directory}\n` +
|
|
515
|
+
` xano sandbox review\n\n` +
|
|
516
|
+
`To enable direct push, go to Workspace Settings → CLI → Allow Direct Workspace Push.\n\n` +
|
|
517
|
+
`Note: Free plan instances don't include sandbox environments, so direct push is always enabled.`);
|
|
518
|
+
}
|
|
468
519
|
}
|
|
469
520
|
catch {
|
|
470
521
|
errorMessage += `\n${errorText}`;
|
|
@@ -635,7 +686,7 @@ Push functions but exclude test files
|
|
|
635
686
|
const color = op.action === 'update' || op.action === 'update_field' ? 'yellow' : 'green';
|
|
636
687
|
const actionLabel = op.action.toUpperCase();
|
|
637
688
|
this.log(` ${ux.colorize(color, actionLabel.padEnd(16))} ${op.type.padEnd(18)} ${op.name}`);
|
|
638
|
-
if (op.details) {
|
|
689
|
+
if (verbose && op.details) {
|
|
639
690
|
this.log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', op.details)}`);
|
|
640
691
|
}
|
|
641
692
|
if (verbose && op.reason) {
|
|
@@ -656,7 +707,7 @@ Push functions but exclude test files
|
|
|
656
707
|
const color = op.action === 'truncate' || op.action === 'alter_field' ? 'yellow' : 'red';
|
|
657
708
|
const actionLabel = op.action.toUpperCase();
|
|
658
709
|
this.log(` ${ux.colorize(color, actionLabel.padEnd(16))} ${op.type.padEnd(18)} ${op.name}`);
|
|
659
|
-
if (op.details) {
|
|
710
|
+
if (verbose && op.details) {
|
|
660
711
|
this.log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', op.details)}`);
|
|
661
712
|
}
|
|
662
713
|
if (verbose && op.reason) {
|
|
@@ -709,6 +760,30 @@ Push functions but exclude test files
|
|
|
709
760
|
}
|
|
710
761
|
return files.sort();
|
|
711
762
|
}
|
|
763
|
+
renderBadIndexes(badIndexes) {
|
|
764
|
+
this.log('');
|
|
765
|
+
this.log(ux.colorize('red', ux.colorize('bold', '=== CRITICAL: Invalid Indexes ===')));
|
|
766
|
+
this.log('');
|
|
767
|
+
this.log(ux.colorize('red', 'The following tables have indexed referencing fields that do not exist in the schema, which may cause related issues.'));
|
|
768
|
+
this.log('');
|
|
769
|
+
for (const idx of badIndexes) {
|
|
770
|
+
this.log(` ${ux.colorize('red', 'CRITICAL'.padEnd(16))} ${'table'.padEnd(18)} ${idx.table}`);
|
|
771
|
+
this.log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', `${idx.indexType} index → field "${idx.field}" does not exist in schema`)}`);
|
|
772
|
+
}
|
|
773
|
+
this.log('');
|
|
774
|
+
}
|
|
775
|
+
renderBadReferences(badRefs) {
|
|
776
|
+
this.log(ux.colorize('yellow', ux.colorize('bold', '=== Unresolved References ===')));
|
|
777
|
+
this.log('');
|
|
778
|
+
this.log(ux.colorize('yellow', "The following references point to objects that don't exist in this push or on the server."));
|
|
779
|
+
this.log(ux.colorize('yellow', 'These will become placeholder statements after import.'));
|
|
780
|
+
this.log('');
|
|
781
|
+
for (const ref of badRefs) {
|
|
782
|
+
this.log(` ${ux.colorize('yellow', 'WARNING'.padEnd(16))} ${ref.sourceType.padEnd(18)} ${ref.source}`);
|
|
783
|
+
this.log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', `${ref.statementType} → ${ref.targetType} "${ref.target}" does not exist`)}`);
|
|
784
|
+
}
|
|
785
|
+
this.log('');
|
|
786
|
+
}
|
|
712
787
|
loadCredentials() {
|
|
713
788
|
const configDir = path.join(os.homedir(), '.xano');
|
|
714
789
|
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A reference from one document to another (e.g., function.run "bad" references function "bad").
|
|
3
|
+
*/
|
|
4
|
+
export interface BadReference {
|
|
5
|
+
/** Name of the source document containing the reference */
|
|
6
|
+
source: string;
|
|
7
|
+
/** Type of the source document (e.g., "function") */
|
|
8
|
+
sourceType: string;
|
|
9
|
+
/** The statement type that creates the reference (e.g., "function.run") */
|
|
10
|
+
statementType: string;
|
|
11
|
+
/** The referenced name that doesn't exist */
|
|
12
|
+
target: string;
|
|
13
|
+
/** The target type being referenced (e.g., "function") */
|
|
14
|
+
targetType: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Build a registry of all defined object names from parsed documents.
|
|
18
|
+
* Returns a Map of type → Set of names.
|
|
19
|
+
*/
|
|
20
|
+
export declare function buildRegistry(documents: Array<{
|
|
21
|
+
content: string;
|
|
22
|
+
}>): Map<string, Set<string>>;
|
|
23
|
+
/**
|
|
24
|
+
* Build a registry of server-known object names from dry-run operations.
|
|
25
|
+
* Any object that appears in the dry-run (create, update, unchanged, delete) exists
|
|
26
|
+
* either locally or on the server.
|
|
27
|
+
*/
|
|
28
|
+
export declare function buildServerRegistry(operations: Array<{
|
|
29
|
+
name: string;
|
|
30
|
+
type: string;
|
|
31
|
+
}>): Map<string, Set<string>>;
|
|
32
|
+
/**
|
|
33
|
+
* Check all documents for cross-references that point to names not in the registry.
|
|
34
|
+
*
|
|
35
|
+
* When serverOperations is provided (from dry-run), references are checked against
|
|
36
|
+
* both local files AND server-known objects, eliminating false positives for objects
|
|
37
|
+
* that exist on the server but aren't in the push set.
|
|
38
|
+
*/
|
|
39
|
+
export declare function checkReferences(documents: Array<{
|
|
40
|
+
content: string;
|
|
41
|
+
filePath: string;
|
|
42
|
+
}>, serverOperations?: Array<{
|
|
43
|
+
name: string;
|
|
44
|
+
type: string;
|
|
45
|
+
}>): BadReference[];
|
|
46
|
+
export interface BadIndex {
|
|
47
|
+
field: string;
|
|
48
|
+
indexType: string;
|
|
49
|
+
table: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Check table documents for indexes that reference fields not in the schema.
|
|
53
|
+
* Parses the XanoScript table format to extract schema field names and index field names.
|
|
54
|
+
*/
|
|
55
|
+
export declare function checkTableIndexes(documents: Array<{
|
|
56
|
+
content: string;
|
|
57
|
+
}>): BadIndex[];
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { parseDocument } from './document-parser.js';
|
|
2
|
+
/**
|
|
3
|
+
* All known cross-reference patterns in XanoScript.
|
|
4
|
+
*
|
|
5
|
+
* Each pattern matches a statement keyword followed by a quoted or unquoted name.
|
|
6
|
+
* The first capture group extracts the name (stripping quotes if present).
|
|
7
|
+
*/
|
|
8
|
+
const REFERENCE_PATTERNS = [
|
|
9
|
+
{ keyword: 'function.run', regex: /^\s*function\.run\s+("(?:[^"\\]|\\.)*"|[^\s{]+)/gm, targetType: 'function' },
|
|
10
|
+
{ keyword: 'function.call', regex: /^\s*function\.call\s+("(?:[^"\\]|\\.)*"|[^\s{]+)/gm, targetType: 'function' },
|
|
11
|
+
{ keyword: 'addon.call', regex: /^\s*addon\.call\s+("(?:[^"\\]|\\.)*"|[^\s{]+)/gm, targetType: 'addon' },
|
|
12
|
+
{ keyword: 'task.call', regex: /^\s*task\.call\s+("(?:[^"\\]|\\.)*"|[^\s{]+)/gm, targetType: 'task' },
|
|
13
|
+
{ keyword: 'tool.call', regex: /^\s*tool\.call\s+("(?:[^"\\]|\\.)*"|[^\s{]+)/gm, targetType: 'tool' },
|
|
14
|
+
{ keyword: 'middleware.call', regex: /^\s*middleware\.call\s+("(?:[^"\\]|\\.)*"|[^\s{]+)/gm, targetType: 'middleware' },
|
|
15
|
+
{ keyword: 'ai.agent.run', regex: /^\s*ai\.agent\.run\s+("(?:[^"\\]|\\.)*"|[^\s{]+)/gm, targetType: 'agent' },
|
|
16
|
+
{ keyword: 'trigger.call', regex: /^\s*trigger\.call\s+("(?:[^"\\]|\\.)*"|[^\s{]+)/gm, targetType: 'trigger' },
|
|
17
|
+
{ keyword: 'action.call', regex: /^\s*action\.call\s+("(?:[^"\\]|\\.)*"|[^\s{]+)/gm, targetType: 'action' },
|
|
18
|
+
{
|
|
19
|
+
keyword: 'workflow_test.call',
|
|
20
|
+
regex: /^\s*workflow_test\.call\s+("(?:[^"\\]|\\.)*"|[^\s{]+)/gm,
|
|
21
|
+
targetType: 'workflow_test',
|
|
22
|
+
},
|
|
23
|
+
// db.* statements reference tables: db.get, db.query, db.add, db.edit, db.add_or_edit, db.delete, db.bulk_add, db.bulk_delete, db.count
|
|
24
|
+
{
|
|
25
|
+
keyword: 'db.*',
|
|
26
|
+
regex: /^\s*db\.(?:get|query|add|edit|add_or_edit|delete|bulk_add|bulk_delete|count)\s+("(?:[^"\\]|\\.)*"|[^\s{]+)/gm,
|
|
27
|
+
targetType: 'table',
|
|
28
|
+
},
|
|
29
|
+
// Schema foreign key references: table = "name" inside field definitions
|
|
30
|
+
{ keyword: 'table (FK)', regex: /\btable\s*=\s*"([^"]*)"/gm, targetType: 'table' },
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* Strip surrounding quotes from a name if present.
|
|
34
|
+
*/
|
|
35
|
+
function stripQuotes(name) {
|
|
36
|
+
if (name.startsWith('"') && name.endsWith('"')) {
|
|
37
|
+
return name.slice(1, -1);
|
|
38
|
+
}
|
|
39
|
+
return name;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Map from XanoScript document types to the canonical type used in the registry.
|
|
43
|
+
* Some types are aliases (agent, mcp_server → toolset bucket, but referenced as "agent").
|
|
44
|
+
*/
|
|
45
|
+
/* eslint-disable camelcase */
|
|
46
|
+
const TYPE_ALIASES = {
|
|
47
|
+
agent: 'agent',
|
|
48
|
+
mcp_server: 'agent',
|
|
49
|
+
toolset: 'agent',
|
|
50
|
+
};
|
|
51
|
+
/* eslint-enable camelcase */
|
|
52
|
+
/**
|
|
53
|
+
* Normalize a document type to its canonical registry type.
|
|
54
|
+
*/
|
|
55
|
+
function normalizeType(type) {
|
|
56
|
+
return TYPE_ALIASES[type] ?? type;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Build a registry of all defined object names from parsed documents.
|
|
60
|
+
* Returns a Map of type → Set of names.
|
|
61
|
+
*/
|
|
62
|
+
export function buildRegistry(documents) {
|
|
63
|
+
const registry = new Map();
|
|
64
|
+
for (const doc of documents) {
|
|
65
|
+
const parsed = parseDocument(doc.content);
|
|
66
|
+
if (!parsed)
|
|
67
|
+
continue;
|
|
68
|
+
const type = normalizeType(parsed.type);
|
|
69
|
+
if (!registry.has(type)) {
|
|
70
|
+
registry.set(type, new Set());
|
|
71
|
+
}
|
|
72
|
+
registry.get(type).add(parsed.name);
|
|
73
|
+
}
|
|
74
|
+
return registry;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Build a registry of server-known object names from dry-run operations.
|
|
78
|
+
* Any object that appears in the dry-run (create, update, unchanged, delete) exists
|
|
79
|
+
* either locally or on the server.
|
|
80
|
+
*/
|
|
81
|
+
export function buildServerRegistry(operations) {
|
|
82
|
+
const registry = new Map();
|
|
83
|
+
for (const op of operations) {
|
|
84
|
+
const type = normalizeType(op.type);
|
|
85
|
+
if (!registry.has(type)) {
|
|
86
|
+
registry.set(type, new Set());
|
|
87
|
+
}
|
|
88
|
+
// Operation names for queries include the verb (e.g., "path/{id} DELETE")
|
|
89
|
+
// but references use just the name, so strip the verb suffix
|
|
90
|
+
const name = op.name.replace(/\s+(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)$/, '');
|
|
91
|
+
registry.get(type).add(name);
|
|
92
|
+
}
|
|
93
|
+
return registry;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Check all documents for cross-references that point to names not in the registry.
|
|
97
|
+
*
|
|
98
|
+
* When serverOperations is provided (from dry-run), references are checked against
|
|
99
|
+
* both local files AND server-known objects, eliminating false positives for objects
|
|
100
|
+
* that exist on the server but aren't in the push set.
|
|
101
|
+
*/
|
|
102
|
+
export function checkReferences(documents, serverOperations) {
|
|
103
|
+
const registry = buildRegistry(documents);
|
|
104
|
+
// Merge server-known names into the registry
|
|
105
|
+
if (serverOperations) {
|
|
106
|
+
const serverRegistry = buildServerRegistry(serverOperations);
|
|
107
|
+
for (const [type, names] of serverRegistry) {
|
|
108
|
+
if (!registry.has(type)) {
|
|
109
|
+
registry.set(type, new Set());
|
|
110
|
+
}
|
|
111
|
+
for (const name of names) {
|
|
112
|
+
registry.get(type).add(name);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const badRefs = [];
|
|
117
|
+
for (const doc of documents) {
|
|
118
|
+
const parsed = parseDocument(doc.content);
|
|
119
|
+
if (!parsed)
|
|
120
|
+
continue;
|
|
121
|
+
for (const pattern of REFERENCE_PATTERNS) {
|
|
122
|
+
// Reset regex state for each document
|
|
123
|
+
pattern.regex.lastIndex = 0;
|
|
124
|
+
let match;
|
|
125
|
+
while ((match = pattern.regex.exec(doc.content)) !== null) {
|
|
126
|
+
const rawName = stripQuotes(match[1]);
|
|
127
|
+
// Skip empty names only for action.call (valid for integration actions)
|
|
128
|
+
if (!rawName && pattern.keyword === 'action.call')
|
|
129
|
+
continue;
|
|
130
|
+
const { targetType } = pattern;
|
|
131
|
+
const knownNames = registry.get(targetType);
|
|
132
|
+
if (!knownNames || !knownNames.has(rawName)) {
|
|
133
|
+
badRefs.push({
|
|
134
|
+
source: parsed.name,
|
|
135
|
+
sourceType: parsed.type,
|
|
136
|
+
statementType: pattern.keyword,
|
|
137
|
+
target: rawName,
|
|
138
|
+
targetType,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return badRefs;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Check table documents for indexes that reference fields not in the schema.
|
|
148
|
+
* Parses the XanoScript table format to extract schema field names and index field names.
|
|
149
|
+
*/
|
|
150
|
+
export function checkTableIndexes(documents) {
|
|
151
|
+
const badIndexes = [];
|
|
152
|
+
for (const doc of documents) {
|
|
153
|
+
const parsed = parseDocument(doc.content);
|
|
154
|
+
if (!parsed || parsed.type !== 'table')
|
|
155
|
+
continue;
|
|
156
|
+
const schemaFields = extractSchemaFields(doc.content);
|
|
157
|
+
const indexes = extractIndexes(doc.content);
|
|
158
|
+
for (const idx of indexes) {
|
|
159
|
+
for (const field of idx.fields) {
|
|
160
|
+
if (!schemaFields.has(field)) {
|
|
161
|
+
badIndexes.push({
|
|
162
|
+
field,
|
|
163
|
+
indexType: idx.type,
|
|
164
|
+
table: parsed.name,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return badIndexes;
|
|
171
|
+
}
|
|
172
|
+
function extractSchemaFields(content) {
|
|
173
|
+
// id and created_at are auto-added during import
|
|
174
|
+
const fields = new Set(['id', 'created_at']);
|
|
175
|
+
// Find the schema block by matching braces
|
|
176
|
+
const schemaStart = content.match(/\bschema\s*\{/);
|
|
177
|
+
if (!schemaStart || schemaStart.index === undefined)
|
|
178
|
+
return fields;
|
|
179
|
+
let depth = 0;
|
|
180
|
+
let blockStart = -1;
|
|
181
|
+
let blockEnd = -1;
|
|
182
|
+
for (let i = schemaStart.index; i < content.length; i++) {
|
|
183
|
+
if (content[i] === '{') {
|
|
184
|
+
if (depth === 0)
|
|
185
|
+
blockStart = i + 1;
|
|
186
|
+
depth++;
|
|
187
|
+
}
|
|
188
|
+
else if (content[i] === '}') {
|
|
189
|
+
depth--;
|
|
190
|
+
if (depth === 0) {
|
|
191
|
+
blockEnd = i;
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (blockStart < 0 || blockEnd < 0)
|
|
197
|
+
return fields;
|
|
198
|
+
const schemaBlock = content.slice(blockStart, blockEnd);
|
|
199
|
+
// Match field declarations: "type name" or "type name?" or "type name?=default"
|
|
200
|
+
const fieldRegex = /^\s*\w+\s+(\w+)[?\s{]/gm;
|
|
201
|
+
let match;
|
|
202
|
+
while ((match = fieldRegex.exec(schemaBlock)) !== null) {
|
|
203
|
+
fields.add(match[1]);
|
|
204
|
+
}
|
|
205
|
+
return fields;
|
|
206
|
+
}
|
|
207
|
+
function extractIndexes(content) {
|
|
208
|
+
const indexes = [];
|
|
209
|
+
// Match the index array: index = [ ... ]
|
|
210
|
+
const indexMatch = content.match(/\bindex\s*=\s*\[([\s\S]*?)\n\s*\]/);
|
|
211
|
+
if (!indexMatch)
|
|
212
|
+
return indexes;
|
|
213
|
+
const indexBlock = indexMatch[1];
|
|
214
|
+
// Match each index object: {type: "btree", field: [{name: "col", op: "desc"}]}
|
|
215
|
+
const entryRegex = /\{([^}]+)\}/g;
|
|
216
|
+
let match;
|
|
217
|
+
while ((match = entryRegex.exec(indexBlock)) !== null) {
|
|
218
|
+
const entry = match[1];
|
|
219
|
+
const typeMatch = entry.match(/type:\s*"(\w+)"/);
|
|
220
|
+
const type = typeMatch ? typeMatch[1] : 'unknown';
|
|
221
|
+
const fieldNames = [];
|
|
222
|
+
const nameRegex = /name:\s*"(\w*)"/g;
|
|
223
|
+
let nameMatch;
|
|
224
|
+
while ((nameMatch = nameRegex.exec(entry)) !== null) {
|
|
225
|
+
fieldNames.push(nameMatch[1]);
|
|
226
|
+
}
|
|
227
|
+
if (fieldNames.length > 0) {
|
|
228
|
+
indexes.push({ fields: fieldNames, type });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return indexes;
|
|
232
|
+
}
|