@xano/cli 0.0.95-beta.1 → 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.
Files changed (86) hide show
  1. package/README.md +22 -9
  2. package/dist/base-command.d.ts +30 -0
  3. package/dist/base-command.js +61 -0
  4. package/dist/commands/auth/index.js +1 -1
  5. package/dist/commands/profile/create/index.js +2 -2
  6. package/dist/commands/profile/edit/index.js +2 -2
  7. package/dist/commands/profile/me/index.js +21 -2
  8. package/dist/commands/profile/wizard/index.js +3 -3
  9. package/dist/commands/profile/workspace/set/index.js +1 -1
  10. package/dist/commands/release/deploy/index.d.ts +17 -0
  11. package/dist/commands/release/deploy/index.js +107 -0
  12. package/dist/commands/{ephemeral → sandbox}/env/delete/index.d.ts +1 -4
  13. package/dist/commands/{ephemeral → sandbox}/env/delete/index.js +18 -36
  14. package/dist/commands/{ephemeral → sandbox}/env/get/index.d.ts +1 -4
  15. package/dist/commands/sandbox/env/get/index.js +63 -0
  16. package/dist/commands/{ephemeral → sandbox}/env/get_all/index.d.ts +1 -4
  17. package/dist/commands/sandbox/env/get_all/index.js +76 -0
  18. package/dist/commands/{ephemeral → sandbox}/env/list/index.d.ts +1 -4
  19. package/dist/commands/sandbox/env/list/index.js +65 -0
  20. package/dist/commands/{ephemeral → sandbox}/env/set/index.d.ts +1 -4
  21. package/dist/commands/sandbox/env/set/index.js +72 -0
  22. package/dist/commands/{ephemeral → sandbox}/env/set_all/index.d.ts +1 -4
  23. package/dist/commands/{ephemeral → sandbox}/env/set_all/index.js +17 -35
  24. package/dist/commands/{ephemeral → sandbox}/get/index.d.ts +1 -4
  25. package/dist/commands/sandbox/get/index.js +61 -0
  26. package/dist/commands/sandbox/impersonate/index.d.ts +5 -0
  27. package/dist/commands/sandbox/impersonate/index.js +5 -0
  28. package/dist/commands/{ephemeral → sandbox}/license/get/index.d.ts +1 -4
  29. package/dist/commands/sandbox/license/get/index.js +76 -0
  30. package/dist/commands/{ephemeral → sandbox}/license/set/index.d.ts +1 -4
  31. package/dist/commands/{ephemeral → sandbox}/license/set/index.js +18 -36
  32. package/dist/commands/{ephemeral → sandbox}/pull/index.d.ts +1 -2
  33. package/dist/commands/{ephemeral → sandbox}/pull/index.js +11 -28
  34. package/dist/commands/{ephemeral → sandbox}/push/index.d.ts +2 -2
  35. package/dist/commands/{ephemeral → sandbox}/push/index.js +37 -31
  36. package/dist/commands/{ephemeral/delete → sandbox/reset}/index.d.ts +1 -5
  37. package/dist/commands/sandbox/reset/index.js +69 -0
  38. package/dist/commands/{ephemeral/impersonate → sandbox/review}/index.d.ts +1 -4
  39. package/dist/commands/{ephemeral/impersonate → sandbox/review}/index.js +15 -33
  40. package/dist/commands/{ephemeral/unit_test/run_all → sandbox/unit_test/list}/index.d.ts +1 -2
  41. package/dist/commands/{ephemeral → sandbox}/unit_test/list/index.js +10 -26
  42. package/dist/commands/{ephemeral → sandbox}/unit_test/run/index.d.ts +1 -2
  43. package/dist/commands/{ephemeral → sandbox}/unit_test/run/index.js +9 -25
  44. package/dist/commands/{ephemeral/unit_test/list → sandbox/unit_test/run_all}/index.d.ts +1 -2
  45. package/dist/commands/{ephemeral → sandbox}/unit_test/run_all/index.js +7 -23
  46. package/dist/commands/{ephemeral/workflow_test/get → sandbox/workflow_test/delete}/index.d.ts +1 -2
  47. package/dist/commands/{ephemeral → sandbox}/workflow_test/delete/index.js +9 -25
  48. package/dist/commands/{ephemeral/workflow_test/run → sandbox/workflow_test/get}/index.d.ts +1 -2
  49. package/dist/commands/{ephemeral → sandbox}/workflow_test/get/index.js +8 -27
  50. package/dist/commands/{ephemeral/workflow_test/run_all → sandbox/workflow_test/list}/index.d.ts +1 -2
  51. package/dist/commands/{ephemeral → sandbox}/workflow_test/list/index.js +11 -27
  52. package/dist/commands/{ephemeral/workflow_test/delete → sandbox/workflow_test/run}/index.d.ts +1 -2
  53. package/dist/commands/{ephemeral → sandbox}/workflow_test/run/index.js +9 -25
  54. package/dist/commands/{ephemeral/workflow_test/list → sandbox/workflow_test/run_all}/index.d.ts +1 -2
  55. package/dist/commands/{ephemeral → sandbox}/workflow_test/run_all/index.js +7 -23
  56. package/dist/commands/tenant/create/index.d.ts +2 -1
  57. package/dist/commands/tenant/create/index.js +23 -6
  58. package/dist/commands/tenant/get/index.js +2 -2
  59. package/dist/commands/tenant/list/index.js +2 -2
  60. package/dist/commands/tenant/push/index.js +0 -34
  61. package/dist/commands/workspace/edit/index.d.ts +1 -0
  62. package/dist/commands/workspace/edit/index.js +16 -6
  63. package/dist/commands/workspace/get/index.js +9 -7
  64. package/dist/commands/workspace/list/index.d.ts +1 -0
  65. package/dist/commands/workspace/list/index.js +14 -7
  66. package/dist/commands/workspace/push/index.d.ts +1 -0
  67. package/dist/commands/workspace/push/index.js +60 -6
  68. package/dist/utils/reference-checker.d.ts +45 -0
  69. package/dist/utils/reference-checker.js +137 -0
  70. package/oclif.manifest.json +2525 -2894
  71. package/package.json +8 -8
  72. package/dist/commands/ephemeral/access/index.d.ts +0 -15
  73. package/dist/commands/ephemeral/access/index.js +0 -78
  74. package/dist/commands/ephemeral/create/index.d.ts +0 -17
  75. package/dist/commands/ephemeral/create/index.js +0 -102
  76. package/dist/commands/ephemeral/delete/index.js +0 -99
  77. package/dist/commands/ephemeral/env/get/index.js +0 -81
  78. package/dist/commands/ephemeral/env/get_all/index.js +0 -94
  79. package/dist/commands/ephemeral/env/list/index.js +0 -83
  80. package/dist/commands/ephemeral/env/set/index.js +0 -90
  81. package/dist/commands/ephemeral/get/index.js +0 -102
  82. package/dist/commands/ephemeral/license/get/index.js +0 -94
  83. package/dist/commands/ephemeral/list/index.d.ts +0 -15
  84. package/dist/commands/ephemeral/list/index.js +0 -109
  85. package/dist/commands/ephemeral/shared/index.d.ts +0 -15
  86. package/dist/commands/ephemeral/shared/index.js +0 -108
@@ -73,8 +73,8 @@ Workspace: my-workspace (ID: 123)
73
73
  try {
74
74
  const response = await this.verboseFetch(apiUrl, {
75
75
  headers: {
76
- 'accept': 'application/json',
77
- 'Authorization': `Bearer ${profile.access_token}`,
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 * 1000).toISOString().split('T')[0];
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 * 1000).toISOString().split('T')[0];
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
- 'accept': 'application/json',
82
- 'Authorization': `Bearer ${profile.access_token}`,
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 {
@@ -30,5 +30,6 @@ export default class Push extends BaseCommand {
30
30
  * type subdirectory name then filename for deterministic ordering.
31
31
  */
32
32
  private collectFiles;
33
+ private renderBadReferences;
33
34
  private loadCredentials;
34
35
  }
@@ -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, {
@@ -282,6 +283,25 @@ 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 to this workspace is disabled.')));
292
+ this.log(ux.colorize('dim', 'To apply changes to the workspace, use the sandbox review flow:'));
293
+ this.log(` ${ux.colorize('cyan', 'xano sandbox push')} ${ux.colorize('dim', '— push changes to your sandbox')}`);
294
+ this.log(` ${ux.colorize('cyan', 'xano sandbox review')} ${ux.colorize('dim', '— edit any logic, inspect the snapshot diff, and promote changes to the workspace')}`);
295
+ this.log('');
296
+ this.log(ux.colorize('dim', 'To enable direct push, go to Workspace Settings → CLI → Allow Direct Workspace Push.'));
297
+ this.log(ux.colorize('dim', 'Note: This setting does not apply to Free plan instances.'));
298
+ this.log('');
299
+ return;
300
+ }
301
+ }
302
+ catch {
303
+ // Not JSON, fall through
304
+ }
285
305
  this.warn(`Push preview failed (${dryRunResponse.status}). Skipping preview.`);
286
306
  if (flags.verbose) {
287
307
  this.log(ux.colorize('dim', errorText));
@@ -306,6 +326,11 @@ Push functions but exclude test files
306
326
  // Check if the server returned a valid dry-run response
307
327
  if (preview && preview.summary) {
308
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
+ }
309
334
  // Check for critical errors that must block the push
310
335
  const criticalOps = preview.operations.filter((op) => op.details?.includes('exception:') || op.details?.includes('mvp:placeholder'));
311
336
  if (criticalOps.length > 0) {
@@ -424,6 +449,14 @@ Push functions but exclude test files
424
449
  }
425
450
  }
426
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
+ }
427
460
  // For partial pushes, filter to only changed documents
428
461
  if (isPartial && dryRunPreview) {
429
462
  const changedKeys = new Set(dryRunPreview.operations
@@ -465,6 +498,15 @@ Push functions but exclude test files
465
498
  if (errorJson.payload?.param) {
466
499
  errorMessage += `\n Parameter: ${errorJson.payload.param}`;
467
500
  }
501
+ // Provide clear guidance when push is disabled
502
+ if (errorJson.message?.includes('Push is disabled')) {
503
+ this.error(`Push is disabled for this workspace.\n\n` +
504
+ `To enable, go to Workspace Settings and turn on "Allow Push".\n` +
505
+ `Note: This setting does not apply to Free plan instances.\n\n` +
506
+ `Alternatively, use sandbox commands:\n` +
507
+ ` xano sandbox push ${args.directory}\n` +
508
+ ` xano sandbox impersonate`);
509
+ }
468
510
  }
469
511
  catch {
470
512
  errorMessage += `\n${errorText}`;
@@ -635,7 +677,7 @@ Push functions but exclude test files
635
677
  const color = op.action === 'update' || op.action === 'update_field' ? 'yellow' : 'green';
636
678
  const actionLabel = op.action.toUpperCase();
637
679
  this.log(` ${ux.colorize(color, actionLabel.padEnd(16))} ${op.type.padEnd(18)} ${op.name}`);
638
- if (op.details) {
680
+ if (verbose && op.details) {
639
681
  this.log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', op.details)}`);
640
682
  }
641
683
  if (verbose && op.reason) {
@@ -656,7 +698,7 @@ Push functions but exclude test files
656
698
  const color = op.action === 'truncate' || op.action === 'alter_field' ? 'yellow' : 'red';
657
699
  const actionLabel = op.action.toUpperCase();
658
700
  this.log(` ${ux.colorize(color, actionLabel.padEnd(16))} ${op.type.padEnd(18)} ${op.name}`);
659
- if (op.details) {
701
+ if (verbose && op.details) {
660
702
  this.log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', op.details)}`);
661
703
  }
662
704
  if (verbose && op.reason) {
@@ -709,6 +751,18 @@ Push functions but exclude test files
709
751
  }
710
752
  return files.sort();
711
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
+ }
712
766
  loadCredentials() {
713
767
  const configDir = path.join(os.homedir(), '.xano');
714
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
+ }