@xano/cli 0.0.95-beta.10 → 0.0.95-beta.12

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.
Files changed (32) hide show
  1. package/dist/base-command.d.ts +5 -0
  2. package/dist/base-command.js +26 -2
  3. package/dist/commands/sandbox/delete/index.d.ts +12 -0
  4. package/dist/commands/sandbox/delete/index.js +71 -0
  5. package/dist/commands/sandbox/env/delete/index.js +4 -2
  6. package/dist/commands/sandbox/env/get/index.js +4 -2
  7. package/dist/commands/sandbox/env/get_all/index.js +4 -2
  8. package/dist/commands/sandbox/env/list/index.js +4 -2
  9. package/dist/commands/sandbox/env/set/index.js +4 -2
  10. package/dist/commands/sandbox/env/set_all/index.js +4 -2
  11. package/dist/commands/sandbox/get/index.js +2 -0
  12. package/dist/commands/sandbox/license/get/index.js +4 -2
  13. package/dist/commands/sandbox/license/set/index.js +4 -2
  14. package/dist/commands/sandbox/pull/index.js +4 -2
  15. package/dist/commands/sandbox/push/index.d.ts +1 -0
  16. package/dist/commands/sandbox/push/index.js +28 -3
  17. package/dist/commands/sandbox/reset/index.js +4 -2
  18. package/dist/commands/sandbox/review/index.js +4 -2
  19. package/dist/commands/sandbox/unit_test/list/index.js +4 -2
  20. package/dist/commands/sandbox/unit_test/run/index.js +4 -2
  21. package/dist/commands/sandbox/unit_test/run_all/index.js +4 -0
  22. package/dist/commands/sandbox/workflow_test/delete/index.js +4 -2
  23. package/dist/commands/sandbox/workflow_test/get/index.js +4 -2
  24. package/dist/commands/sandbox/workflow_test/list/index.js +4 -2
  25. package/dist/commands/sandbox/workflow_test/run/index.js +4 -2
  26. package/dist/commands/sandbox/workflow_test/run_all/index.js +4 -0
  27. package/dist/commands/workspace/push/index.d.ts +1 -0
  28. package/dist/commands/workspace/push/index.js +30 -4
  29. package/dist/utils/reference-checker.d.ts +45 -0
  30. package/dist/utils/reference-checker.js +145 -0
  31. package/oclif.manifest.json +708 -654
  32. package/package.json +1 -1
@@ -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
- // Request delete info in dry-run so we can show remote-only items (skip for partial)
254
- if (!isPartial) {
255
- dryRunParams.set('delete', 'true');
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,145 @@
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
+ }