@xano/cli 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +112 -3
  2. package/dist/base-command.d.ts +43 -36
  3. package/dist/base-command.js +223 -160
  4. package/dist/commands/auth/index.d.ts +1 -1
  5. package/dist/commands/auth/index.js +7 -7
  6. package/dist/commands/branch/create/index.d.ts +1 -1
  7. package/dist/commands/branch/create/index.js +1 -1
  8. package/dist/commands/ephemeral/create/index.d.ts +18 -0
  9. package/dist/commands/ephemeral/create/index.js +103 -0
  10. package/dist/commands/ephemeral/delete/index.d.ts +18 -0
  11. package/dist/commands/ephemeral/delete/index.js +99 -0
  12. package/dist/commands/ephemeral/edit/index.d.ts +18 -0
  13. package/dist/commands/ephemeral/edit/index.js +109 -0
  14. package/dist/commands/ephemeral/get/index.d.ts +21 -0
  15. package/dist/commands/ephemeral/get/index.js +125 -0
  16. package/dist/commands/ephemeral/impersonate/index.d.ts +20 -0
  17. package/dist/commands/ephemeral/impersonate/index.js +120 -0
  18. package/dist/commands/ephemeral/list/index.d.ts +19 -0
  19. package/dist/commands/ephemeral/list/index.js +123 -0
  20. package/dist/commands/ephemeral/pull/index.d.ts +19 -0
  21. package/dist/commands/ephemeral/pull/index.js +163 -0
  22. package/dist/commands/ephemeral/push/index.d.ts +35 -0
  23. package/dist/commands/ephemeral/push/index.js +223 -0
  24. package/dist/commands/ephemeral/static_host/build/delete/index.d.ts +20 -0
  25. package/dist/commands/ephemeral/static_host/build/delete/index.js +121 -0
  26. package/dist/commands/ephemeral/static_host/build/get/index.d.ts +18 -0
  27. package/dist/commands/ephemeral/static_host/build/get/index.js +137 -0
  28. package/dist/commands/ephemeral/static_host/build/list/index.d.ts +18 -0
  29. package/dist/commands/ephemeral/static_host/build/list/index.js +137 -0
  30. package/dist/commands/ephemeral/static_host/build/pull/index.d.ts +53 -0
  31. package/dist/commands/ephemeral/static_host/build/pull/index.js +308 -0
  32. package/dist/commands/ephemeral/static_host/build/push/index.d.ts +24 -0
  33. package/dist/commands/ephemeral/static_host/build/push/index.js +250 -0
  34. package/dist/commands/ephemeral/static_host/create/index.d.ts +18 -0
  35. package/dist/commands/ephemeral/static_host/create/index.js +92 -0
  36. package/dist/commands/ephemeral/static_host/deploy/index.d.ts +19 -0
  37. package/dist/commands/ephemeral/static_host/deploy/index.js +112 -0
  38. package/dist/commands/ephemeral/static_host/edit/index.d.ts +24 -0
  39. package/dist/commands/ephemeral/static_host/edit/index.js +158 -0
  40. package/dist/commands/ephemeral/static_host/get/index.d.ts +19 -0
  41. package/dist/commands/ephemeral/static_host/get/index.js +101 -0
  42. package/dist/commands/ephemeral/static_host/list/index.d.ts +17 -0
  43. package/dist/commands/ephemeral/static_host/list/index.js +132 -0
  44. package/dist/commands/flatten/index.d.ts +34 -0
  45. package/dist/commands/flatten/index.js +140 -0
  46. package/dist/commands/function/create/index.js +1 -1
  47. package/dist/commands/function/edit/index.js +26 -32
  48. package/dist/commands/function/get/index.js +2 -3
  49. package/dist/commands/function/run/index.d.ts +1 -0
  50. package/dist/commands/function/run/index.js +19 -6
  51. package/dist/commands/profile/create/index.d.ts +1 -1
  52. package/dist/commands/profile/create/index.js +7 -7
  53. package/dist/commands/profile/me/index.js +1 -1
  54. package/dist/commands/profile/use/index.js +0 -4
  55. package/dist/commands/profile/wizard/index.d.ts +1 -1
  56. package/dist/commands/profile/wizard/index.js +12 -10
  57. package/dist/commands/profile/workspace/set/index.js +1 -1
  58. package/dist/commands/release/edit/index.js +2 -2
  59. package/dist/commands/release/export/index.js +1 -2
  60. package/dist/commands/release/import/index.js +1 -1
  61. package/dist/commands/release/pull/index.d.ts +0 -6
  62. package/dist/commands/release/pull/index.js +13 -99
  63. package/dist/commands/release/push/index.js +2 -2
  64. package/dist/commands/sandbox/env/get_all/index.js +1 -1
  65. package/dist/commands/sandbox/env/set_all/index.js +1 -1
  66. package/dist/commands/sandbox/get/index.js +1 -1
  67. package/dist/commands/sandbox/license/get/index.js +1 -1
  68. package/dist/commands/sandbox/license/set/index.js +1 -1
  69. package/dist/commands/sandbox/pull/index.d.ts +0 -1
  70. package/dist/commands/sandbox/pull/index.js +13 -81
  71. package/dist/commands/sandbox/push/index.d.ts +1 -1
  72. package/dist/commands/sandbox/push/index.js +5 -5
  73. package/dist/commands/static_host/build/create/index.d.ts +1 -1
  74. package/dist/commands/static_host/build/create/index.js +2 -2
  75. package/dist/commands/static_host/build/pull/index.js +1 -1
  76. package/dist/commands/static_host/build/push/index.js +1 -1
  77. package/dist/commands/static_host/edit/index.js +1 -1
  78. package/dist/commands/tenant/backup/export/index.js +1 -2
  79. package/dist/commands/tenant/backup/import/index.js +1 -1
  80. package/dist/commands/tenant/cluster/create/index.js +1 -1
  81. package/dist/commands/tenant/cluster/license/get/index.js +1 -1
  82. package/dist/commands/tenant/cluster/license/set/index.js +1 -1
  83. package/dist/commands/tenant/create/index.d.ts +1 -1
  84. package/dist/commands/tenant/create/index.js +6 -6
  85. package/dist/commands/tenant/deploy_platform/index.js +1 -1
  86. package/dist/commands/tenant/edit/index.js +7 -7
  87. package/dist/commands/tenant/env/get_all/index.js +1 -1
  88. package/dist/commands/tenant/env/set_all/index.js +1 -1
  89. package/dist/commands/tenant/impersonate/index.d.ts +1 -1
  90. package/dist/commands/tenant/impersonate/index.js +14 -14
  91. package/dist/commands/tenant/license/get/index.js +1 -1
  92. package/dist/commands/tenant/license/set/index.js +1 -1
  93. package/dist/commands/tenant/pull/index.d.ts +0 -6
  94. package/dist/commands/tenant/pull/index.js +14 -100
  95. package/dist/commands/workspace/git/pull/index.d.ts +4 -4
  96. package/dist/commands/workspace/git/pull/index.js +158 -97
  97. package/dist/commands/workspace/pull/index.d.ts +1 -7
  98. package/dist/commands/workspace/pull/index.js +17 -103
  99. package/dist/help.js +4 -2
  100. package/dist/update-check.d.ts +1 -1
  101. package/dist/update-check.js +1 -1
  102. package/dist/utils/document-parser.d.ts +116 -0
  103. package/dist/utils/document-parser.js +311 -4
  104. package/dist/utils/flatten.d.ts +36 -0
  105. package/dist/utils/flatten.js +68 -0
  106. package/dist/utils/knowledge-sync.d.ts +1 -0
  107. package/dist/utils/knowledge-sync.js +11 -6
  108. package/dist/utils/microservice-wait.d.ts +53 -0
  109. package/dist/utils/microservice-wait.js +84 -0
  110. package/dist/utils/multidoc-push.d.ts +88 -0
  111. package/dist/utils/multidoc-push.js +378 -15
  112. package/dist/utils/paging.d.ts +2 -1
  113. package/dist/utils/paging.js +0 -1
  114. package/dist/utils/reference-checker.js +14 -3
  115. package/oclif.manifest.json +5264 -3170
  116. package/package.json +10 -1
@@ -0,0 +1,132 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import BaseCommand from '../../../../base-command.js';
3
+ import { buildPagingJson, buildPagingParams, formatPagingFooter, normalizeListResponse, pagingFlags } from '../../../../utils/paging.js';
4
+ export default class EphemeralStaticHostList extends BaseCommand {
5
+ static args = {
6
+ tenant_name: Args.string({
7
+ description: 'Ephemeral tenant name',
8
+ required: true,
9
+ }),
10
+ };
11
+ static description = 'List all static hosts for an ephemeral tenant from the Xano Metadata API';
12
+ static examples = [
13
+ `$ xano ephemeral static_host list e4f2-9ab1-xyz1 -w 40
14
+ Available static hosts:
15
+ - my-static-host (ID: 1)
16
+ - another-host (ID: 2)
17
+ `,
18
+ `$ xano ephemeral static_host list e4f2-9ab1-xyz1 --profile production
19
+ Available static hosts:
20
+ - my-static-host (ID: 1)
21
+ - another-host (ID: 2)
22
+ `,
23
+ `$ xano ephemeral static_host list e4f2-9ab1-xyz1 -w 40 --output json
24
+ {
25
+ "count": 1,
26
+ "page": 1,
27
+ "total": 3,
28
+ "items": [
29
+ {
30
+ "id": 1,
31
+ "name": "my-static-host"
32
+ }
33
+ ]
34
+ }
35
+ `,
36
+ `$ xano ephemeral static_host list e4f2-9ab1-xyz1 -p staging -o json --page 2
37
+ [
38
+ {
39
+ "id": 3,
40
+ "name": "static-host-3"
41
+ }
42
+ ]
43
+ `,
44
+ ];
45
+ static flags = {
46
+ ...BaseCommand.baseFlags,
47
+ output: Flags.string({
48
+ char: 'o',
49
+ default: 'summary',
50
+ description: 'Output format',
51
+ options: ['summary', 'json'],
52
+ required: false,
53
+ }),
54
+ ...pagingFlags('page-only-envelope', { fixedPerPage: 100 }),
55
+ workspace: Flags.string({
56
+ char: 'w',
57
+ description: 'Workspace ID (optional if set in profile)',
58
+ required: false,
59
+ }),
60
+ };
61
+ async run() {
62
+ const { args, flags } = await this.parse(EphemeralStaticHostList);
63
+ const { profile, profileName } = this.resolveProfile(flags);
64
+ const tenantName = args.tenant_name;
65
+ // Determine workspace_id from flag or profile
66
+ let workspaceId;
67
+ if (flags.workspace) {
68
+ workspaceId = flags.workspace;
69
+ }
70
+ else if (profile.workspace) {
71
+ workspaceId = profile.workspace;
72
+ }
73
+ else {
74
+ this.error(`Workspace ID is required. Either:\n` +
75
+ ` 1. Provide it as a flag: xano ephemeral static_host list <tenant_name> -w <workspace_id>\n` +
76
+ ` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
77
+ }
78
+ // Build query parameters
79
+ const queryParams = new URLSearchParams(buildPagingParams(flags, 'page-only-envelope'));
80
+ // Construct the API URL
81
+ const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/static_host?${queryParams.toString()}`;
82
+ // Fetch static hosts from the API
83
+ try {
84
+ const response = await this.verboseFetch(apiUrl, {
85
+ headers: {
86
+ 'accept': 'application/json',
87
+ 'Authorization': `Bearer ${profile.access_token}`,
88
+ },
89
+ method: 'GET',
90
+ }, flags.verbose, profile.access_token);
91
+ if (!response.ok) {
92
+ const errorText = await response.text();
93
+ this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
94
+ }
95
+ const list = normalizeListResponse(await response.json(), ['static_hosts']);
96
+ const staticHosts = list.items;
97
+ // Output results
98
+ if (flags.output === 'json') {
99
+ this.log(JSON.stringify(buildPagingJson(list, { page: flags.page, tier: 'page-only-envelope' }), null, 2));
100
+ }
101
+ else {
102
+ // summary format
103
+ if (staticHosts.length === 0) {
104
+ this.log('No static hosts found');
105
+ }
106
+ else {
107
+ this.log('Available static hosts:');
108
+ for (const host of staticHosts) {
109
+ if (host.id === undefined) {
110
+ this.log(` - ${host.name}`);
111
+ }
112
+ else {
113
+ const domainInfo = host.domain ? ` - ${host.domain}` : '';
114
+ this.log(` - ${host.name} (ID: ${host.id})${domainInfo}`);
115
+ }
116
+ }
117
+ }
118
+ const footer = formatPagingFooter(list, { noun: 'static host', page: flags.page, tier: 'page-only-envelope' });
119
+ if (footer)
120
+ this.log(footer);
121
+ }
122
+ }
123
+ catch (error) {
124
+ if (error instanceof Error) {
125
+ this.error(`Failed to fetch static hosts: ${error.message}`);
126
+ }
127
+ else {
128
+ this.error(`Failed to fetch static hosts: ${String(error)}`);
129
+ }
130
+ }
131
+ }
132
+ }
@@ -0,0 +1,34 @@
1
+ import { Command } from '@oclif/core';
2
+ /**
3
+ * Split a multi-document `.xs` bundle into the standard one-document-per-file
4
+ * layout that the push pipeline requires.
5
+ *
6
+ * A hand-authored bundle keeps every object (workspace, tables, queries,
7
+ * microservices, …) in a single file joined by `---`. That is convenient to
8
+ * edit but unsupported by `push`: the partial-diff filter and GUID writeback
9
+ * both parse only the first document in a file, so such a bundle silently
10
+ * pushes nothing (partial mode) or corrupts GUIDs (full mode). `flatten` turns
11
+ * it back into the exact tree `pull` would have produced, using the shared
12
+ * placement logic so the two never drift.
13
+ *
14
+ * Not a BaseCommand: this is a purely local, offline transform — no profile,
15
+ * token, or network — so it extends oclif's Command directly.
16
+ */
17
+ export default class Flatten extends Command {
18
+ static args: {
19
+ input: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
20
+ };
21
+ static description: string;
22
+ static examples: string[];
23
+ static flags: {
24
+ 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
25
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
26
+ 'keep-source': import("@oclif/core/interfaces").BooleanFlag<boolean>;
27
+ output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
28
+ };
29
+ run(): Promise<void>;
30
+ /** Recursively collect `.xs` files under a dir that actually hold >1 document. */
31
+ private collectMultiDocFiles;
32
+ /** Flatten a single bundle file. Returns the number of documents written. */
33
+ private flattenOne;
34
+ }
@@ -0,0 +1,140 @@
1
+ import { Args, Command, Flags, ux } from '@oclif/core';
2
+ import * as fs from 'node:fs';
3
+ import { join, relative, resolve } from 'node:path';
4
+ import { flattenBundleFile } from '../../utils/flatten.js';
5
+ /**
6
+ * Split a multi-document `.xs` bundle into the standard one-document-per-file
7
+ * layout that the push pipeline requires.
8
+ *
9
+ * A hand-authored bundle keeps every object (workspace, tables, queries,
10
+ * microservices, …) in a single file joined by `---`. That is convenient to
11
+ * edit but unsupported by `push`: the partial-diff filter and GUID writeback
12
+ * both parse only the first document in a file, so such a bundle silently
13
+ * pushes nothing (partial mode) or corrupts GUIDs (full mode). `flatten` turns
14
+ * it back into the exact tree `pull` would have produced, using the shared
15
+ * placement logic so the two never drift.
16
+ *
17
+ * Not a BaseCommand: this is a purely local, offline transform — no profile,
18
+ * token, or network — so it extends oclif's Command directly.
19
+ */
20
+ export default class Flatten extends Command {
21
+ static args = {
22
+ input: Args.string({
23
+ description: 'A multi-document .xs file, or a directory containing them, to split',
24
+ required: true,
25
+ }),
26
+ };
27
+ static description = 'Split a multi-document .xs bundle (documents joined by `---`) into the standard ' +
28
+ 'one-document-per-file layout that push requires. Deletes the original bundle by ' +
29
+ 'default (use --keep-source to keep it). Purely local — no network.';
30
+ static examples = [
31
+ `$ xano flatten secret/pdf-micro/multidoc.xs
32
+ Split the bundle in place into per-document files (removes multidoc.xs)`,
33
+ `$ xano flatten ./bundle.xs -o ./workspace
34
+ Write the split files under ./workspace instead of alongside the bundle`,
35
+ `$ xano flatten secret/pdf-micro/multidoc.xs --dry-run
36
+ Preview the file layout without writing anything`,
37
+ `$ xano flatten ./dir-of-bundles --keep-source
38
+ Flatten every multi-doc .xs under a directory, keeping the originals`,
39
+ ];
40
+ static flags = {
41
+ 'dry-run': Flags.boolean({
42
+ default: false,
43
+ description: 'Show the resulting file layout without writing anything',
44
+ }),
45
+ force: Flags.boolean({
46
+ char: 'f',
47
+ default: false,
48
+ description: 'Overwrite existing destination files instead of erroring',
49
+ }),
50
+ 'keep-source': Flags.boolean({
51
+ default: false,
52
+ description: 'Keep the original multi-document file(s) (default: delete after a successful split)',
53
+ }),
54
+ output: Flags.string({
55
+ char: 'o',
56
+ description: 'Output directory (default: the directory each bundle file lives in)',
57
+ }),
58
+ };
59
+ async run() {
60
+ const { args, flags } = await this.parse(Flatten);
61
+ const inputPath = resolve(args.input);
62
+ if (!fs.existsSync(inputPath)) {
63
+ this.error(`Not found: ${inputPath}`);
64
+ }
65
+ // Resolve the set of bundle files to flatten.
66
+ const bundleFiles = fs.statSync(inputPath).isDirectory()
67
+ ? this.collectMultiDocFiles(inputPath)
68
+ : [inputPath];
69
+ if (bundleFiles.length === 0) {
70
+ this.error(fs.statSync(inputPath).isDirectory()
71
+ ? `No multi-document .xs files found under ${inputPath}`
72
+ : `${inputPath} is not a .xs file`);
73
+ }
74
+ let totalWritten = 0;
75
+ for (const bundleFile of bundleFiles) {
76
+ totalWritten += this.flattenOne(bundleFile, flags);
77
+ }
78
+ if (flags['dry-run']) {
79
+ this.log('');
80
+ this.log(ux.colorize('dim', `Dry run — ${totalWritten} file(s) would be written. Nothing changed.`));
81
+ }
82
+ else {
83
+ this.log('');
84
+ this.log(`Flattened ${bundleFiles.length} bundle(s) into ${totalWritten} file(s).`);
85
+ }
86
+ }
87
+ /** Recursively collect `.xs` files under a dir that actually hold >1 document. */
88
+ collectMultiDocFiles(dir) {
89
+ const out = [];
90
+ const walk = (d) => {
91
+ for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
92
+ const full = join(d, entry.name);
93
+ if (entry.isDirectory()) {
94
+ walk(full);
95
+ }
96
+ else if (entry.isFile() && entry.name.endsWith('.xs')) {
97
+ const content = fs.readFileSync(full, 'utf8');
98
+ if (/^---$/m.test(content))
99
+ out.push(full);
100
+ }
101
+ }
102
+ };
103
+ walk(dir);
104
+ return out.sort();
105
+ }
106
+ /** Flatten a single bundle file. Returns the number of documents written. */
107
+ flattenOne(bundleFile, flags) {
108
+ const rel = relative(process.cwd(), bundleFile) || bundleFile;
109
+ let result;
110
+ try {
111
+ // Buffer the plan so we can print a header before it only when there's work.
112
+ const planLines = [];
113
+ result = flattenBundleFile(bundleFile, {
114
+ dryRun: flags['dry-run'],
115
+ force: flags.force,
116
+ keepSource: flags['keep-source'],
117
+ log: (msg) => planLines.push(msg),
118
+ outputDir: flags.output,
119
+ });
120
+ if (result.skipped === 'empty') {
121
+ this.warn(`No parseable documents in ${rel}; skipping.`);
122
+ return 0;
123
+ }
124
+ if (result.skipped === 'single') {
125
+ this.warn(`${rel} holds a single document; nothing to split. Skipping.`);
126
+ return 0;
127
+ }
128
+ this.log('');
129
+ this.log(ux.colorize('bold', `${rel} → ${result.written.length} documents`));
130
+ for (const l of planLines) {
131
+ // Colorize the WRITE marker; dim the incidental notes.
132
+ this.log(l.startsWith(' WRITE') ? l.replace('WRITE', ux.colorize('green', 'WRITE')) : ux.colorize('dim', l));
133
+ }
134
+ }
135
+ catch (error) {
136
+ this.error(error.message);
137
+ }
138
+ return result.written.length;
139
+ }
140
+ }
@@ -2,7 +2,7 @@ import { Flags } from '@oclif/core';
2
2
  import { execSync } from 'node:child_process';
3
3
  import * as fs from 'node:fs';
4
4
  import * as os from 'node:os';
5
- import * as path from 'node:path';
5
+ import path from 'node:path';
6
6
  import BaseCommand from '../../../base-command.js';
7
7
  export default class FunctionCreate extends BaseCommand {
8
8
  static args = {};
@@ -3,7 +3,7 @@ import inquirer from 'inquirer';
3
3
  import { execSync } from 'node:child_process';
4
4
  import * as fs from 'node:fs';
5
5
  import * as os from 'node:os';
6
- import * as path from 'node:path';
6
+ import path from 'node:path';
7
7
  import BaseCommand, { buildUserAgent } from '../../../base-command.js';
8
8
  export default class FunctionEdit extends BaseCommand {
9
9
  static args = {
@@ -120,8 +120,7 @@ Name: my_function
120
120
  ` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
121
121
  }
122
122
  // If function_id is not provided, prompt user to select from list
123
- let functionId;
124
- functionId = args.function_id ? args.function_id : await this.promptForFunctionId(profile, workspaceId);
123
+ const functionId = args.function_id || (await this.promptForFunctionId(profile, workspaceId));
125
124
  // Read XanoScript content
126
125
  let xanoscript;
127
126
  if (flags.file) {
@@ -221,14 +220,14 @@ Name: my_function
221
220
  // Get the EDITOR environment variable
222
221
  const editor = process.env.EDITOR || process.env.VISUAL;
223
222
  if (!editor) {
224
- this.error('No editor configured. Please set the EDITOR or VISUAL environment variable.\n' + 'Example: export EDITOR=vim');
223
+ this.error('No editor configured. Please set the EDITOR or VISUAL environment variable.\nExample: export EDITOR=vim');
225
224
  }
226
225
  // Validate editor executable exists
227
226
  try {
228
227
  execSync(`which ${editor.split(' ')[0]}`, { stdio: 'ignore' });
229
228
  }
230
229
  catch {
231
- this.error(`Editor '${editor}' not found. Please set EDITOR to a valid editor.\n` + 'Example: export EDITOR=vim');
230
+ this.error(`Editor '${editor}' not found. Please set EDITOR to a valid editor.\nExample: export EDITOR=vim`);
232
231
  }
233
232
  // Read the original file
234
233
  let originalContent;
@@ -321,35 +320,30 @@ Name: my_function
321
320
  include_xanoscript: 'true',
322
321
  });
323
322
  const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/function/${functionId}?${queryParams.toString()}`;
324
- try {
325
- const response = await fetch(apiUrl, {
326
- headers: {
327
- 'User-Agent': buildUserAgent(this.config.version),
328
- accept: 'application/json',
329
- Authorization: `Bearer ${profile.access_token}`,
330
- },
331
- method: 'GET',
332
- });
333
- if (!response.ok) {
334
- const errorText = await response.text();
335
- throw new Error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
323
+ const response = await fetch(apiUrl, {
324
+ headers: {
325
+ accept: 'application/json',
326
+ Authorization: `Bearer ${profile.access_token}`,
327
+ 'User-Agent': buildUserAgent(this.config.version),
328
+ },
329
+ method: 'GET',
330
+ });
331
+ if (!response.ok) {
332
+ const errorText = await response.text();
333
+ throw new Error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
334
+ }
335
+ const result = (await response.json());
336
+ // Handle xanoscript as an object with status and value
337
+ if (result.xanoscript) {
338
+ if (result.xanoscript.status === 'ok' && result.xanoscript.value !== undefined) {
339
+ return result.xanoscript.value;
336
340
  }
337
- const result = (await response.json());
338
- // Handle xanoscript as an object with status and value
339
- if (result.xanoscript) {
340
- if (result.xanoscript.status === 'ok' && result.xanoscript.value !== undefined) {
341
- return result.xanoscript.value;
342
- }
343
- if (typeof result.xanoscript === 'string') {
344
- return result.xanoscript;
345
- }
346
- throw new Error(`Invalid xanoscript format: ${JSON.stringify(result.xanoscript)}`);
341
+ if (typeof result.xanoscript === 'string') {
342
+ return result.xanoscript;
347
343
  }
348
- return '';
349
- }
350
- catch (error) {
351
- throw error;
344
+ throw new Error(`Invalid xanoscript format: ${JSON.stringify(result.xanoscript)}`);
352
345
  }
346
+ return '';
353
347
  }
354
348
  async promptForFunctionId(profile, workspaceId) {
355
349
  try {
@@ -365,9 +359,9 @@ Name: my_function
365
359
  const listUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/function?${queryParams.toString()}`;
366
360
  const response = await fetch(listUrl, {
367
361
  headers: {
368
- 'User-Agent': buildUserAgent(this.config.version),
369
362
  accept: 'application/json',
370
363
  Authorization: `Bearer ${profile.access_token}`,
364
+ 'User-Agent': buildUserAgent(this.config.version),
371
365
  },
372
366
  method: 'GET',
373
367
  });
@@ -89,8 +89,7 @@ function yo {
89
89
  ` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
90
90
  }
91
91
  // If function_id is not provided, prompt user to select from list
92
- let functionId;
93
- functionId = args.function_id ? args.function_id : await this.promptForFunctionId(profile, workspaceId);
92
+ const functionId = args.function_id || (await this.promptForFunctionId(profile, workspaceId));
94
93
  // Build query parameters
95
94
  // Automatically set include_xanoscript to true if output format is xs
96
95
  const includeXanoscript = flags.output === 'xs' ? true : flags.include_xanoscript;
@@ -178,9 +177,9 @@ function yo {
178
177
  const listUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/function?${queryParams.toString()}`;
179
178
  const response = await fetch(listUrl, {
180
179
  headers: {
181
- 'User-Agent': buildUserAgent(this.config.version),
182
180
  accept: 'application/json',
183
181
  Authorization: `Bearer ${profile.access_token}`,
182
+ 'User-Agent': buildUserAgent(this.config.version),
184
183
  },
185
184
  method: 'GET',
186
185
  });
@@ -8,6 +8,7 @@ export default class FunctionRun extends BaseCommand {
8
8
  static flags: {
9
9
  branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
10
  data: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ datasource: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
12
  json: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
13
  logs: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
14
  name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -17,6 +17,8 @@ export default class FunctionRun extends BaseCommand {
17
17
  `$ xano function:run calcScore --json @payload.json --data env=staging`,
18
18
  `$ echo '{"email":"jo@x.com"}' | xano function:run calcScore --stdin -o json | jq .result`,
19
19
  `$ xano function:run calcScore --branch dev --logs`,
20
+ `$ xano function:run calcScore --datasource test
21
+ # Runs against the 'test' data source instead of 'live'`,
20
22
  ];
21
23
  static flags = {
22
24
  ...BaseCommand.baseFlags,
@@ -30,6 +32,10 @@ export default class FunctionRun extends BaseCommand {
30
32
  multiple: true,
31
33
  required: false,
32
34
  }),
35
+ datasource: Flags.string({
36
+ description: 'Data source to run against, e.g. test (defaults to live)',
37
+ required: false,
38
+ }),
33
39
  json: Flags.string({
34
40
  description: "Input as a JSON object: inline, @file.json, or '-' for stdin",
35
41
  exclusive: ['stdin'],
@@ -106,13 +112,21 @@ export default class FunctionRun extends BaseCommand {
106
112
  const body = { input, name: functionName };
107
113
  if (branch)
108
114
  body.branch = branch;
115
+ const headers = {
116
+ 'accept': 'application/json',
117
+ 'Authorization': `Bearer ${profile.access_token}`,
118
+ 'Content-Type': 'application/json',
119
+ };
120
+ // The Metadata API resolves the workspace's tables against the data source
121
+ // named by X-Data-Source (defaulting to `live`), so the run reads and writes
122
+ // the same tables a request carrying that header would. An unknown label is
123
+ // rejected server-side with "Invalid data source."
124
+ const dataSource = flags.datasource?.trim();
125
+ if (dataSource)
126
+ headers['X-Data-Source'] = dataSource;
109
127
  const response = await this.verboseFetch(apiUrl, {
110
128
  body: JSON.stringify(body),
111
- headers: {
112
- 'accept': 'application/json',
113
- 'Authorization': `Bearer ${profile.access_token}`,
114
- 'Content-Type': 'application/json',
115
- },
129
+ headers,
116
130
  method: 'POST',
117
131
  }, flags.verbose, profile.access_token);
118
132
  if (!response.ok) {
@@ -153,7 +167,6 @@ export default class FunctionRun extends BaseCommand {
153
167
  */
154
168
  async fetchFunctionByName(opts) {
155
169
  const { branch, name, profile, verbose, workspaceId } = opts;
156
- // eslint-disable-next-line camelcase -- external Metadata API query param
157
170
  const params = new URLSearchParams({ per_page: '100', search: name });
158
171
  if (branch)
159
172
  params.set('branch', branch);
@@ -6,10 +6,10 @@ export default class ProfileCreate extends Command {
6
6
  static description: string;
7
7
  static examples: string[];
8
8
  static flags: {
9
- config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
9
  access_token: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
10
  account_origin: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
11
  branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
13
  default: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
14
  insecure: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
15
  instance_origin: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
@@ -1,7 +1,7 @@
1
1
  import { Args, Command, Flags } from '@oclif/core';
2
2
  import * as yaml from 'js-yaml';
3
3
  import * as fs from 'node:fs';
4
- import * as path from 'node:path';
4
+ import path from 'node:path';
5
5
  import { resolveCredentialsPath } from '../../../base-command.js';
6
6
  export default class ProfileCreate extends Command {
7
7
  static args = {
@@ -30,12 +30,6 @@ Profile 'selfhosted' created successfully at ~/.xano/credentials.yaml
30
30
  `,
31
31
  ];
32
32
  static flags = {
33
- config: Flags.string({
34
- char: 'c',
35
- description: 'Path to credentials file (default: ~/.xano/credentials.yaml)',
36
- env: 'XANO_CONFIG',
37
- required: false,
38
- }),
39
33
  access_token: Flags.string({
40
34
  char: 't',
41
35
  description: 'Access token for the Xano Metadata API',
@@ -51,6 +45,12 @@ Profile 'selfhosted' created successfully at ~/.xano/credentials.yaml
51
45
  description: 'Branch name',
52
46
  required: false,
53
47
  }),
48
+ config: Flags.string({
49
+ char: 'c',
50
+ description: 'Path to credentials file (default: ~/.xano/credentials.yaml)',
51
+ env: 'XANO_CONFIG',
52
+ required: false,
53
+ }),
54
54
  default: Flags.boolean({
55
55
  default: false,
56
56
  description: 'Set this profile as the default',
@@ -171,7 +171,7 @@ User Information:
171
171
  const credentialsPath = this.getCredentialsPath();
172
172
  // Check if credentials file exists
173
173
  if (!fs.existsSync(credentialsPath)) {
174
- this.error(`Credentials file not found at ${credentialsPath}\n` + `Create a profile using 'xano profile:create'`);
174
+ this.error(`Credentials file not found at ${credentialsPath}\nCreate a profile using 'xano profile:create'`);
175
175
  }
176
176
  // Read credentials file
177
177
  try {
@@ -68,7 +68,6 @@ Added profile.yaml to .gitignore
68
68
  `,
69
69
  ];
70
70
  static flags = {
71
- // eslint-disable-next-line camelcase
72
71
  account_origin: Flags.string({
73
72
  char: 'a',
74
73
  description: 'Override account origin for this project',
@@ -90,7 +89,6 @@ Added profile.yaml to .gitignore
90
89
  description: 'Add (or skip adding) profile.yaml to .gitignore without prompting',
91
90
  required: false,
92
91
  }),
93
- // eslint-disable-next-line camelcase
94
92
  instance_origin: Flags.string({
95
93
  char: 'i',
96
94
  description: 'Override instance origin for this project',
@@ -108,10 +106,8 @@ Added profile.yaml to .gitignore
108
106
  const config = { profile: args.name };
109
107
  if (flags.workspace)
110
108
  config.workspace = flags.workspace;
111
- // eslint-disable-next-line camelcase
112
109
  if (flags.instance_origin)
113
110
  config.instance_origin = flags.instance_origin;
114
- // eslint-disable-next-line camelcase
115
111
  if (flags.account_origin)
116
112
  config.account_origin = flags.account_origin;
117
113
  if (flags.branch)
@@ -9,10 +9,10 @@ export default class ProfileWizard extends Command {
9
9
  origin: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
10
  };
11
11
  run(): Promise<void>;
12
- private getHeaders;
13
12
  private fetchBranches;
14
13
  private fetchInstances;
15
14
  private fetchWorkspaces;
16
15
  private getDefaultProfileName;
16
+ private getHeaders;
17
17
  private saveProfile;
18
18
  }
@@ -1,9 +1,8 @@
1
- import { ExitPromptError } from '@inquirer/core';
2
1
  import { Command, Flags } from '@oclif/core';
3
2
  import inquirer from 'inquirer';
4
3
  import * as yaml from 'js-yaml';
5
4
  import * as fs from 'node:fs';
6
- import * as path from 'node:path';
5
+ import path from 'node:path';
7
6
  import { buildUserAgent, resolveCredentialsPath } from '../../../base-command.js';
8
7
  export default class ProfileWizard extends Command {
9
8
  static description = 'Create a new profile configuration using an interactive wizard';
@@ -188,20 +187,16 @@ Profile 'production' created successfully at ~/.xano/credentials.yaml
188
187
  this.log(`✓ Profile '${profileName}' created successfully!`);
189
188
  }
190
189
  catch (error) {
191
- if (error instanceof ExitPromptError) {
190
+ // Ctrl+C at an inquirer prompt throws ExitPromptError. Match on the name
191
+ // rather than `instanceof`: inquirer bundles its own copy of
192
+ // @inquirer/core, so the thrown class won't match an imported one.
193
+ if (error?.name === 'ExitPromptError') {
192
194
  this.log('Wizard cancelled.');
193
195
  process.exit(0);
194
196
  }
195
197
  throw error;
196
198
  }
197
199
  }
198
- getHeaders(accessToken) {
199
- return {
200
- 'User-Agent': buildUserAgent(this.config.version),
201
- accept: 'application/json',
202
- ...(accessToken && { Authorization: `Bearer ${accessToken}` }),
203
- };
204
- }
205
200
  async fetchBranches(accessToken, origin, workspaceId) {
206
201
  const response = await fetch(`${origin}/api:meta/workspace/${workspaceId}/branch`, {
207
202
  headers: this.getHeaders(accessToken),
@@ -325,6 +320,13 @@ Profile 'production' created successfully at ~/.xano/credentials.yaml
325
320
  return 'default';
326
321
  }
327
322
  }
323
+ getHeaders(accessToken) {
324
+ return {
325
+ accept: 'application/json',
326
+ 'User-Agent': buildUserAgent(this.config.version),
327
+ ...(accessToken && { Authorization: `Bearer ${accessToken}` }),
328
+ };
329
+ }
328
330
  async saveProfile(profile, setAsDefault = false, configPath) {
329
331
  const credentialsPath = resolveCredentialsPath(configPath);
330
332
  const configDir = path.dirname(credentialsPath);