@xano/cli 0.0.95-beta.10 → 0.0.95-beta.11
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/base-command.d.ts +5 -0
- package/dist/base-command.js +26 -2
- package/dist/commands/sandbox/env/delete/index.js +2 -2
- package/dist/commands/sandbox/env/get/index.js +2 -2
- package/dist/commands/sandbox/env/get_all/index.js +2 -2
- package/dist/commands/sandbox/env/list/index.js +2 -2
- package/dist/commands/sandbox/env/set/index.js +2 -2
- package/dist/commands/sandbox/env/set_all/index.js +2 -2
- package/dist/commands/sandbox/license/get/index.js +2 -2
- package/dist/commands/sandbox/license/set/index.js +2 -2
- package/dist/commands/sandbox/pull/index.js +2 -2
- package/dist/commands/sandbox/push/index.d.ts +1 -0
- package/dist/commands/sandbox/push/index.js +26 -3
- package/dist/commands/sandbox/reset/index.js +2 -2
- package/dist/commands/sandbox/review/index.js +2 -2
- package/dist/commands/sandbox/unit_test/list/index.js +2 -2
- package/dist/commands/sandbox/unit_test/run/index.js +2 -2
- package/dist/commands/sandbox/workflow_test/delete/index.js +2 -2
- package/dist/commands/sandbox/workflow_test/get/index.js +2 -2
- package/dist/commands/sandbox/workflow_test/list/index.js +2 -2
- package/dist/commands/sandbox/workflow_test/run/index.js +2 -2
- package/dist/commands/workspace/push/index.d.ts +1 -0
- package/dist/commands/workspace/push/index.js +30 -4
- package/dist/utils/reference-checker.d.ts +45 -0
- package/dist/utils/reference-checker.js +137 -0
- package/oclif.manifest.json +1914 -1914
- package/package.json +1 -1
package/dist/base-command.d.ts
CHANGED
|
@@ -63,6 +63,11 @@ export default abstract class BaseCommand extends Command {
|
|
|
63
63
|
profile: ProfileConfig;
|
|
64
64
|
profileName: string;
|
|
65
65
|
};
|
|
66
|
+
/**
|
|
67
|
+
* Parse an API error response and return a clean error message.
|
|
68
|
+
* Extracts the message from JSON responses and adds context for common errors.
|
|
69
|
+
*/
|
|
70
|
+
protected parseApiError(response: Response, fallbackPrefix: string): Promise<string>;
|
|
66
71
|
/**
|
|
67
72
|
* Make an HTTP request with optional verbose logging.
|
|
68
73
|
* Use this for all Metadata API calls to support the --verbose flag.
|
package/dist/base-command.js
CHANGED
|
@@ -117,8 +117,8 @@ export default class BaseCommand extends Command {
|
|
|
117
117
|
method: 'GET',
|
|
118
118
|
}, verbose, profile.access_token);
|
|
119
119
|
if (!response.ok) {
|
|
120
|
-
const
|
|
121
|
-
this.error(
|
|
120
|
+
const message = await this.parseApiError(response, 'Failed to get sandbox environment');
|
|
121
|
+
this.error(message);
|
|
122
122
|
}
|
|
123
123
|
return (await response.json());
|
|
124
124
|
}
|
|
@@ -140,6 +140,30 @@ export default class BaseCommand extends Command {
|
|
|
140
140
|
}
|
|
141
141
|
return { profile, profileName };
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Parse an API error response and return a clean error message.
|
|
145
|
+
* Extracts the message from JSON responses and adds context for common errors.
|
|
146
|
+
*/
|
|
147
|
+
async parseApiError(response, fallbackPrefix) {
|
|
148
|
+
const errorText = await response.text();
|
|
149
|
+
let message = `${fallbackPrefix} (${response.status})`;
|
|
150
|
+
try {
|
|
151
|
+
const errorJson = JSON.parse(errorText);
|
|
152
|
+
if (errorJson.message) {
|
|
153
|
+
message = errorJson.message;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
if (errorText) {
|
|
158
|
+
message += `\n${errorText}`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Provide guidance when sandbox access is denied (free plan restriction)
|
|
162
|
+
if (response.status === 500 && message === 'Access Denied.') {
|
|
163
|
+
message = 'Sandbox is not available on the Free plan. Upgrade your plan to use sandbox features.';
|
|
164
|
+
}
|
|
165
|
+
return message;
|
|
166
|
+
}
|
|
143
167
|
/**
|
|
144
168
|
* Make an HTTP request with optional verbose logging.
|
|
145
169
|
* Use this for all Metadata API calls to support the --verbose flag.
|
|
@@ -52,8 +52,8 @@ Environment variable 'DATABASE_URL' deleted
|
|
|
52
52
|
method: 'DELETE',
|
|
53
53
|
}, flags.verbose, profile.access_token);
|
|
54
54
|
if (!response.ok) {
|
|
55
|
-
const
|
|
56
|
-
this.error(
|
|
55
|
+
const message = await this.parseApiError(response, 'API request failed');
|
|
56
|
+
this.error(message);
|
|
57
57
|
}
|
|
58
58
|
if (flags.output === 'json') {
|
|
59
59
|
this.log(JSON.stringify({ deleted: true, env_name: envName }, null, 2));
|
|
@@ -37,8 +37,8 @@ postgres://localhost:5432/mydb
|
|
|
37
37
|
method: 'GET',
|
|
38
38
|
}, flags.verbose, profile.access_token);
|
|
39
39
|
if (!response.ok) {
|
|
40
|
-
const
|
|
41
|
-
this.error(
|
|
40
|
+
const message = await this.parseApiError(response, 'API request failed');
|
|
41
|
+
this.error(message);
|
|
42
42
|
}
|
|
43
43
|
const envVar = (await response.json());
|
|
44
44
|
if (flags.output === 'json') {
|
|
@@ -46,8 +46,8 @@ Environment variables saved to env_<tenant>.yaml
|
|
|
46
46
|
method: 'GET',
|
|
47
47
|
}, flags.verbose, profile.access_token);
|
|
48
48
|
if (!response.ok) {
|
|
49
|
-
const
|
|
50
|
-
this.error(
|
|
49
|
+
const message = await this.parseApiError(response, 'API request failed');
|
|
50
|
+
this.error(message);
|
|
51
51
|
}
|
|
52
52
|
const envMap = (await response.json());
|
|
53
53
|
if (flags.output === 'json') {
|
|
@@ -33,8 +33,8 @@ Environment variables for sandbox environment:
|
|
|
33
33
|
method: 'GET',
|
|
34
34
|
}, flags.verbose, profile.access_token);
|
|
35
35
|
if (!response.ok) {
|
|
36
|
-
const
|
|
37
|
-
this.error(
|
|
36
|
+
const message = await this.parseApiError(response, 'API request failed');
|
|
37
|
+
this.error(message);
|
|
38
38
|
}
|
|
39
39
|
const data = (await response.json());
|
|
40
40
|
if (flags.output === 'json') {
|
|
@@ -49,8 +49,8 @@ Environment variable 'DATABASE_URL' set
|
|
|
49
49
|
method: 'PATCH',
|
|
50
50
|
}, flags.verbose, profile.access_token);
|
|
51
51
|
if (!response.ok) {
|
|
52
|
-
const
|
|
53
|
-
this.error(
|
|
52
|
+
const message = await this.parseApiError(response, 'API request failed');
|
|
53
|
+
this.error(message);
|
|
54
54
|
}
|
|
55
55
|
if (flags.output === 'json') {
|
|
56
56
|
const result = await response.json();
|
|
@@ -57,8 +57,8 @@ Reads from env_<tenant>.yaml
|
|
|
57
57
|
method: 'PUT',
|
|
58
58
|
}, flags.verbose, profile.access_token);
|
|
59
59
|
if (!response.ok) {
|
|
60
|
-
const
|
|
61
|
-
this.error(
|
|
60
|
+
const message = await this.parseApiError(response, 'API request failed');
|
|
61
|
+
this.error(message);
|
|
62
62
|
}
|
|
63
63
|
if (flags.output === 'json') {
|
|
64
64
|
const result = await response.json();
|
|
@@ -45,8 +45,8 @@ License saved to license_<tenant>.yaml
|
|
|
45
45
|
method: 'GET',
|
|
46
46
|
}, flags.verbose, profile.access_token);
|
|
47
47
|
if (!response.ok) {
|
|
48
|
-
const
|
|
49
|
-
this.error(
|
|
48
|
+
const message = await this.parseApiError(response, 'API request failed');
|
|
49
|
+
this.error(message);
|
|
50
50
|
}
|
|
51
51
|
const license = await response.json();
|
|
52
52
|
const licenseContent = typeof license === 'string' ? license : JSON.stringify(license, null, 2);
|
|
@@ -66,8 +66,8 @@ Reads from license_<tenant>.yaml
|
|
|
66
66
|
method: 'POST',
|
|
67
67
|
}, flags.verbose, profile.access_token);
|
|
68
68
|
if (!response.ok) {
|
|
69
|
-
const
|
|
70
|
-
this.error(
|
|
69
|
+
const message = await this.parseApiError(response, 'API request failed');
|
|
70
|
+
this.error(message);
|
|
71
71
|
}
|
|
72
72
|
const result = await response.json();
|
|
73
73
|
if (flags.output === 'json') {
|
|
@@ -55,8 +55,8 @@ Pulled 42 documents from sandbox environment to ./my-sandbox
|
|
|
55
55
|
method: 'GET',
|
|
56
56
|
}, flags.verbose, profile.access_token);
|
|
57
57
|
if (!response.ok) {
|
|
58
|
-
const
|
|
59
|
-
this.error(
|
|
58
|
+
const message = await this.parseApiError(response, 'API request failed');
|
|
59
|
+
this.error(message);
|
|
60
60
|
}
|
|
61
61
|
responseText = await response.text();
|
|
62
62
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { Args, Flags } from '@oclif/core';
|
|
2
|
-
import BaseCommand from '../../../base-command.js';
|
|
3
|
-
import { findFilesWithGuid } from '../../../utils/document-parser.js';
|
|
1
|
+
import { Args, Flags, ux } from '@oclif/core';
|
|
4
2
|
import * as fs from 'node:fs';
|
|
5
3
|
import * as path from 'node:path';
|
|
4
|
+
import BaseCommand from '../../../base-command.js';
|
|
5
|
+
import { findFilesWithGuid } from '../../../utils/document-parser.js';
|
|
6
|
+
import { checkReferences } from '../../../utils/reference-checker.js';
|
|
6
7
|
export default class SandboxPush extends BaseCommand {
|
|
7
8
|
static args = {
|
|
8
9
|
directory: Args.string({
|
|
@@ -66,6 +67,11 @@ Pushed 42 documents to sandbox environment from ./my-workspace
|
|
|
66
67
|
if (documentEntries.length === 0) {
|
|
67
68
|
this.error(`All .xs files in ${args.directory} are empty`);
|
|
68
69
|
}
|
|
70
|
+
// Check for bad cross-references within the local file set
|
|
71
|
+
const badRefs = checkReferences(documentEntries);
|
|
72
|
+
if (badRefs.length > 0) {
|
|
73
|
+
this.renderBadReferences(badRefs);
|
|
74
|
+
}
|
|
69
75
|
const multidoc = documentEntries.map((d) => d.content).join('\n---\n');
|
|
70
76
|
const queryParams = new URLSearchParams({
|
|
71
77
|
env: flags.env.toString(),
|
|
@@ -98,6 +104,10 @@ Pushed 42 documents to sandbox environment from ./my-workspace
|
|
|
98
104
|
catch {
|
|
99
105
|
errorMessage += `\n${errorText}`;
|
|
100
106
|
}
|
|
107
|
+
// Provide guidance when sandbox access is denied (free plan restriction)
|
|
108
|
+
if (response.status === 500 && errorMessage.includes('Access Denied')) {
|
|
109
|
+
this.error('Sandbox is not available on the Free plan. Upgrade your plan to use sandbox features.');
|
|
110
|
+
}
|
|
101
111
|
const guidMatch = errorMessage.match(/Duplicate \w+ guid: (\S+)/);
|
|
102
112
|
if (guidMatch) {
|
|
103
113
|
const dupeFiles = findFilesWithGuid(documentEntries, guidMatch[1]);
|
|
@@ -138,4 +148,17 @@ Pushed 42 documents to sandbox environment from ./my-workspace
|
|
|
138
148
|
}
|
|
139
149
|
return files.sort();
|
|
140
150
|
}
|
|
151
|
+
renderBadReferences(badRefs) {
|
|
152
|
+
this.log('');
|
|
153
|
+
this.log(ux.colorize('yellow', ux.colorize('bold', '=== Unresolved References ===')));
|
|
154
|
+
this.log('');
|
|
155
|
+
this.log(ux.colorize('yellow', "The following references point to objects that don't exist in this push or on the server."));
|
|
156
|
+
this.log(ux.colorize('yellow', 'These will become placeholder statements after import.'));
|
|
157
|
+
this.log('');
|
|
158
|
+
for (const ref of badRefs) {
|
|
159
|
+
this.log(` ${ux.colorize('yellow', 'WARNING'.padEnd(16))} ${ref.sourceType.padEnd(18)} ${ref.source}`);
|
|
160
|
+
this.log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', `${ref.statementType} → ${ref.targetType} "${ref.target}" does not exist`)}`);
|
|
161
|
+
}
|
|
162
|
+
this.log('');
|
|
163
|
+
}
|
|
141
164
|
}
|
|
@@ -39,8 +39,8 @@ Sandbox environment has been reset.
|
|
|
39
39
|
method: 'POST',
|
|
40
40
|
}, flags.verbose, profile.access_token);
|
|
41
41
|
if (!response.ok) {
|
|
42
|
-
const
|
|
43
|
-
this.error(
|
|
42
|
+
const message = await this.parseApiError(response, 'API request failed');
|
|
43
|
+
this.error(message);
|
|
44
44
|
}
|
|
45
45
|
this.log('Sandbox environment has been reset.');
|
|
46
46
|
}
|
|
@@ -40,8 +40,8 @@ Review session started!
|
|
|
40
40
|
method: 'GET',
|
|
41
41
|
}, flags.verbose, profile.access_token);
|
|
42
42
|
if (!response.ok) {
|
|
43
|
-
const
|
|
44
|
-
this.error(
|
|
43
|
+
const message = await this.parseApiError(response, 'API request failed');
|
|
44
|
+
this.error(message);
|
|
45
45
|
}
|
|
46
46
|
const result = (await response.json());
|
|
47
47
|
if (!result._ti) {
|
|
@@ -48,8 +48,8 @@ Unit tests:
|
|
|
48
48
|
method: 'GET',
|
|
49
49
|
}, flags.verbose, profile.access_token);
|
|
50
50
|
if (!response.ok) {
|
|
51
|
-
const
|
|
52
|
-
this.error(
|
|
51
|
+
const message = await this.parseApiError(response, 'API request failed');
|
|
52
|
+
this.error(message);
|
|
53
53
|
}
|
|
54
54
|
const data = (await response.json());
|
|
55
55
|
let tests;
|
|
@@ -42,8 +42,8 @@ Result: PASS
|
|
|
42
42
|
method: 'POST',
|
|
43
43
|
}, flags.verbose, profile.access_token);
|
|
44
44
|
if (!response.ok) {
|
|
45
|
-
const
|
|
46
|
-
this.error(
|
|
45
|
+
const message = await this.parseApiError(response, 'API request failed');
|
|
46
|
+
this.error(message);
|
|
47
47
|
}
|
|
48
48
|
const result = (await response.json());
|
|
49
49
|
if (flags.output === 'json') {
|
|
@@ -37,8 +37,8 @@ Deleted workflow test 42
|
|
|
37
37
|
method: 'DELETE',
|
|
38
38
|
}, flags.verbose, profile.access_token);
|
|
39
39
|
if (!response.ok) {
|
|
40
|
-
const
|
|
41
|
-
this.error(
|
|
40
|
+
const message = await this.parseApiError(response, 'API request failed');
|
|
41
|
+
this.error(message);
|
|
42
42
|
}
|
|
43
43
|
if (flags.output === 'json') {
|
|
44
44
|
this.log(JSON.stringify({ deleted: true, workflow_test_id: args.workflow_test_id }, null, 2));
|
|
@@ -32,8 +32,8 @@ export default class SandboxWorkflowTestGet extends BaseCommand {
|
|
|
32
32
|
method: 'GET',
|
|
33
33
|
}, flags.verbose, profile.access_token);
|
|
34
34
|
if (!response.ok) {
|
|
35
|
-
const
|
|
36
|
-
this.error(
|
|
35
|
+
const message = await this.parseApiError(response, 'API request failed');
|
|
36
|
+
this.error(message);
|
|
37
37
|
}
|
|
38
38
|
const test = await response.json();
|
|
39
39
|
if (flags.output === 'json') {
|
|
@@ -41,8 +41,8 @@ Workflow tests:
|
|
|
41
41
|
method: 'GET',
|
|
42
42
|
}, flags.verbose, profile.access_token);
|
|
43
43
|
if (!response.ok) {
|
|
44
|
-
const
|
|
45
|
-
this.error(
|
|
44
|
+
const message = await this.parseApiError(response, 'API request failed');
|
|
45
|
+
this.error(message);
|
|
46
46
|
}
|
|
47
47
|
const data = (await response.json());
|
|
48
48
|
let tests;
|
|
@@ -42,8 +42,8 @@ Result: PASS (0.25s)
|
|
|
42
42
|
method: 'POST',
|
|
43
43
|
}, flags.verbose, profile.access_token);
|
|
44
44
|
if (!response.ok) {
|
|
45
|
-
const
|
|
46
|
-
this.error(
|
|
45
|
+
const message = await this.parseApiError(response, 'API request failed');
|
|
46
|
+
this.error(message);
|
|
47
47
|
}
|
|
48
48
|
const result = (await response.json());
|
|
49
49
|
if (flags.output === 'json') {
|
|
@@ -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 } 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, {
|
|
@@ -325,6 +326,11 @@ Push functions but exclude test files
|
|
|
325
326
|
// Check if the server returned a valid dry-run response
|
|
326
327
|
if (preview && preview.summary) {
|
|
327
328
|
this.renderPreview(preview, shouldDelete, workspaceId, flags.verbose, isPartial);
|
|
329
|
+
// Check for bad cross-references, using dry-run operations to avoid false positives
|
|
330
|
+
const badRefs = checkReferences(documentEntries, preview.operations);
|
|
331
|
+
if (badRefs.length > 0) {
|
|
332
|
+
this.renderBadReferences(badRefs);
|
|
333
|
+
}
|
|
328
334
|
// Check for critical errors that must block the push
|
|
329
335
|
const criticalOps = preview.operations.filter((op) => op.details?.includes('exception:') || op.details?.includes('mvp:placeholder'));
|
|
330
336
|
if (criticalOps.length > 0) {
|
|
@@ -443,6 +449,14 @@ Push functions but exclude test files
|
|
|
443
449
|
}
|
|
444
450
|
}
|
|
445
451
|
}
|
|
452
|
+
// Show bad references in force mode (preview mode shows them inline)
|
|
453
|
+
if (flags.force) {
|
|
454
|
+
const badRefs = checkReferences(documentEntries);
|
|
455
|
+
if (badRefs.length > 0) {
|
|
456
|
+
this.log('');
|
|
457
|
+
this.renderBadReferences(badRefs);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
446
460
|
// For partial pushes, filter to only changed documents
|
|
447
461
|
if (isPartial && dryRunPreview) {
|
|
448
462
|
const changedKeys = new Set(dryRunPreview.operations
|
|
@@ -737,6 +751,18 @@ Push functions but exclude test files
|
|
|
737
751
|
}
|
|
738
752
|
return files.sort();
|
|
739
753
|
}
|
|
754
|
+
renderBadReferences(badRefs) {
|
|
755
|
+
this.log(ux.colorize('yellow', ux.colorize('bold', '=== Unresolved References ===')));
|
|
756
|
+
this.log('');
|
|
757
|
+
this.log(ux.colorize('yellow', "The following references point to objects that don't exist in this push or on the server."));
|
|
758
|
+
this.log(ux.colorize('yellow', 'These will become placeholder statements after import.'));
|
|
759
|
+
this.log('');
|
|
760
|
+
for (const ref of badRefs) {
|
|
761
|
+
this.log(` ${ux.colorize('yellow', 'WARNING'.padEnd(16))} ${ref.sourceType.padEnd(18)} ${ref.source}`);
|
|
762
|
+
this.log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', `${ref.statementType} → ${ref.targetType} "${ref.target}" does not exist`)}`);
|
|
763
|
+
}
|
|
764
|
+
this.log('');
|
|
765
|
+
}
|
|
740
766
|
loadCredentials() {
|
|
741
767
|
const configDir = path.join(os.homedir(), '.xano');
|
|
742
768
|
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
@@ -0,0 +1,45 @@
|
|
|
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[];
|
|
@@ -0,0 +1,137 @@
|
|
|
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
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* Strip surrounding quotes from a name if present.
|
|
26
|
+
*/
|
|
27
|
+
function stripQuotes(name) {
|
|
28
|
+
if (name.startsWith('"') && name.endsWith('"')) {
|
|
29
|
+
return name.slice(1, -1);
|
|
30
|
+
}
|
|
31
|
+
return name;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Map from XanoScript document types to the canonical type used in the registry.
|
|
35
|
+
* Some types are aliases (agent, mcp_server → toolset bucket, but referenced as "agent").
|
|
36
|
+
*/
|
|
37
|
+
/* eslint-disable camelcase */
|
|
38
|
+
const TYPE_ALIASES = {
|
|
39
|
+
agent: 'agent',
|
|
40
|
+
mcp_server: 'agent',
|
|
41
|
+
toolset: 'agent',
|
|
42
|
+
};
|
|
43
|
+
/* eslint-enable camelcase */
|
|
44
|
+
/**
|
|
45
|
+
* Normalize a document type to its canonical registry type.
|
|
46
|
+
*/
|
|
47
|
+
function normalizeType(type) {
|
|
48
|
+
return TYPE_ALIASES[type] ?? type;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build a registry of all defined object names from parsed documents.
|
|
52
|
+
* Returns a Map of type → Set of names.
|
|
53
|
+
*/
|
|
54
|
+
export function buildRegistry(documents) {
|
|
55
|
+
const registry = new Map();
|
|
56
|
+
for (const doc of documents) {
|
|
57
|
+
const parsed = parseDocument(doc.content);
|
|
58
|
+
if (!parsed)
|
|
59
|
+
continue;
|
|
60
|
+
const type = normalizeType(parsed.type);
|
|
61
|
+
if (!registry.has(type)) {
|
|
62
|
+
registry.set(type, new Set());
|
|
63
|
+
}
|
|
64
|
+
registry.get(type).add(parsed.name);
|
|
65
|
+
}
|
|
66
|
+
return registry;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Build a registry of server-known object names from dry-run operations.
|
|
70
|
+
* Any object that appears in the dry-run (create, update, unchanged, delete) exists
|
|
71
|
+
* either locally or on the server.
|
|
72
|
+
*/
|
|
73
|
+
export function buildServerRegistry(operations) {
|
|
74
|
+
const registry = new Map();
|
|
75
|
+
for (const op of operations) {
|
|
76
|
+
const type = normalizeType(op.type);
|
|
77
|
+
if (!registry.has(type)) {
|
|
78
|
+
registry.set(type, new Set());
|
|
79
|
+
}
|
|
80
|
+
// Operation names for queries include the verb (e.g., "path/{id} DELETE")
|
|
81
|
+
// but references use just the name, so strip the verb suffix
|
|
82
|
+
const name = op.name.replace(/\s+(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)$/, '');
|
|
83
|
+
registry.get(type).add(name);
|
|
84
|
+
}
|
|
85
|
+
return registry;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Check all documents for cross-references that point to names not in the registry.
|
|
89
|
+
*
|
|
90
|
+
* When serverOperations is provided (from dry-run), references are checked against
|
|
91
|
+
* both local files AND server-known objects, eliminating false positives for objects
|
|
92
|
+
* that exist on the server but aren't in the push set.
|
|
93
|
+
*/
|
|
94
|
+
export function checkReferences(documents, serverOperations) {
|
|
95
|
+
const registry = buildRegistry(documents);
|
|
96
|
+
// Merge server-known names into the registry
|
|
97
|
+
if (serverOperations) {
|
|
98
|
+
const serverRegistry = buildServerRegistry(serverOperations);
|
|
99
|
+
for (const [type, names] of serverRegistry) {
|
|
100
|
+
if (!registry.has(type)) {
|
|
101
|
+
registry.set(type, new Set());
|
|
102
|
+
}
|
|
103
|
+
for (const name of names) {
|
|
104
|
+
registry.get(type).add(name);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const badRefs = [];
|
|
109
|
+
for (const doc of documents) {
|
|
110
|
+
const parsed = parseDocument(doc.content);
|
|
111
|
+
if (!parsed)
|
|
112
|
+
continue;
|
|
113
|
+
for (const pattern of REFERENCE_PATTERNS) {
|
|
114
|
+
// Reset regex state for each document
|
|
115
|
+
pattern.regex.lastIndex = 0;
|
|
116
|
+
let match;
|
|
117
|
+
while ((match = pattern.regex.exec(doc.content)) !== null) {
|
|
118
|
+
const rawName = stripQuotes(match[1]);
|
|
119
|
+
// Skip empty names (e.g., action.call "" is valid for integration actions)
|
|
120
|
+
if (!rawName)
|
|
121
|
+
continue;
|
|
122
|
+
const { targetType } = pattern;
|
|
123
|
+
const knownNames = registry.get(targetType);
|
|
124
|
+
if (!knownNames || !knownNames.has(rawName)) {
|
|
125
|
+
badRefs.push({
|
|
126
|
+
source: parsed.name,
|
|
127
|
+
sourceType: parsed.type,
|
|
128
|
+
statementType: pattern.keyword,
|
|
129
|
+
target: rawName,
|
|
130
|
+
targetType,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return badRefs;
|
|
137
|
+
}
|