@xano/cli 1.0.6-beta.1 → 1.0.6-beta.10

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 (83) hide show
  1. package/README.md +96 -4
  2. package/dist/commands/branch/list/index.js +5 -1
  3. package/dist/commands/ephemeral/impersonate/index.d.ts +1 -0
  4. package/dist/commands/ephemeral/impersonate/index.js +11 -3
  5. package/dist/commands/ephemeral/list/index.js +2 -1
  6. package/dist/commands/ephemeral/pull/index.d.ts +0 -6
  7. package/dist/commands/ephemeral/pull/index.js +10 -95
  8. package/dist/commands/ephemeral/push/index.d.ts +10 -0
  9. package/dist/commands/ephemeral/push/index.js +77 -0
  10. package/dist/commands/ephemeral/static_host/build/list/index.d.ts +2 -3
  11. package/dist/commands/ephemeral/static_host/build/list/index.js +19 -40
  12. package/dist/commands/ephemeral/static_host/list/index.d.ts +2 -3
  13. package/dist/commands/ephemeral/static_host/list/index.js +20 -40
  14. package/dist/commands/flatten/index.d.ts +34 -0
  15. package/dist/commands/flatten/index.js +140 -0
  16. package/dist/commands/function/list/index.d.ts +4 -4
  17. package/dist/commands/function/list/index.js +30 -38
  18. package/dist/commands/knowledge/list/index.js +2 -1
  19. package/dist/commands/platform/list/index.js +13 -14
  20. package/dist/commands/release/list/index.js +13 -14
  21. package/dist/commands/release/pull/index.d.ts +0 -6
  22. package/dist/commands/release/pull/index.js +10 -95
  23. package/dist/commands/sandbox/env/list/index.js +4 -0
  24. package/dist/commands/sandbox/pull/index.d.ts +0 -1
  25. package/dist/commands/sandbox/pull/index.js +10 -77
  26. package/dist/commands/sandbox/unit_test/list/index.d.ts +2 -0
  27. package/dist/commands/sandbox/unit_test/list/index.js +9 -14
  28. package/dist/commands/sandbox/unit_test/run_all/index.d.ts +1 -0
  29. package/dist/commands/sandbox/unit_test/run_all/index.js +67 -37
  30. package/dist/commands/sandbox/workflow_test/list/index.d.ts +2 -0
  31. package/dist/commands/sandbox/workflow_test/list/index.js +9 -14
  32. package/dist/commands/sandbox/workflow_test/run_all/index.d.ts +1 -0
  33. package/dist/commands/sandbox/workflow_test/run_all/index.js +65 -35
  34. package/dist/commands/static_host/build/list/index.d.ts +2 -3
  35. package/dist/commands/static_host/build/list/index.js +19 -40
  36. package/dist/commands/static_host/build/pull/index.js +12 -9
  37. package/dist/commands/static_host/list/index.d.ts +2 -3
  38. package/dist/commands/static_host/list/index.js +20 -40
  39. package/dist/commands/static_host/migrate/index.js +14 -10
  40. package/dist/commands/tenant/backup/list/index.js +13 -14
  41. package/dist/commands/tenant/cluster/list/index.js +9 -10
  42. package/dist/commands/tenant/env/list/index.js +4 -0
  43. package/dist/commands/tenant/list/index.js +19 -20
  44. package/dist/commands/tenant/pull/index.d.ts +0 -6
  45. package/dist/commands/tenant/pull/index.js +10 -95
  46. package/dist/commands/tenant/snapshot/list/index.js +5 -1
  47. package/dist/commands/tenant/unit_test/list/index.d.ts +4 -2
  48. package/dist/commands/tenant/unit_test/list/index.js +9 -14
  49. package/dist/commands/tenant/unit_test/run_all/index.d.ts +1 -0
  50. package/dist/commands/tenant/unit_test/run_all/index.js +67 -37
  51. package/dist/commands/tenant/workflow_test/list/index.d.ts +4 -2
  52. package/dist/commands/tenant/workflow_test/list/index.js +9 -14
  53. package/dist/commands/tenant/workflow_test/run_all/index.d.ts +1 -0
  54. package/dist/commands/tenant/workflow_test/run_all/index.js +65 -35
  55. package/dist/commands/unit_test/list/index.d.ts +3 -1
  56. package/dist/commands/unit_test/list/index.js +9 -14
  57. package/dist/commands/unit_test/run_all/index.d.ts +1 -0
  58. package/dist/commands/unit_test/run_all/index.js +68 -40
  59. package/dist/commands/workflow_test/list/index.d.ts +3 -1
  60. package/dist/commands/workflow_test/list/index.js +10 -13
  61. package/dist/commands/workflow_test/run_all/index.d.ts +1 -0
  62. package/dist/commands/workflow_test/run_all/index.js +67 -36
  63. package/dist/commands/workspace/git/pull/index.js +53 -3
  64. package/dist/commands/workspace/list/index.js +7 -13
  65. package/dist/commands/workspace/pull/index.d.ts +0 -6
  66. package/dist/commands/workspace/pull/index.js +10 -95
  67. package/dist/utils/concurrency.d.ts +29 -0
  68. package/dist/utils/concurrency.js +54 -0
  69. package/dist/utils/document-parser.d.ts +116 -0
  70. package/dist/utils/document-parser.js +274 -1
  71. package/dist/utils/flatten.d.ts +36 -0
  72. package/dist/utils/flatten.js +68 -0
  73. package/dist/utils/knowledge-sync.d.ts +1 -0
  74. package/dist/utils/knowledge-sync.js +11 -5
  75. package/dist/utils/microservice-wait.d.ts +53 -0
  76. package/dist/utils/microservice-wait.js +83 -0
  77. package/dist/utils/multidoc-push.d.ts +12 -0
  78. package/dist/utils/multidoc-push.js +84 -0
  79. package/dist/utils/paging.d.ts +145 -0
  80. package/dist/utils/paging.js +199 -0
  81. package/dist/utils/reference-checker.js +13 -0
  82. package/oclif.manifest.json +2762 -2550
  83. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../../base-command.js';
3
+ import { buildPagingJson, buildPagingParams, formatPagingFooter, normalizeListResponse, pagingFlags } from '../../../../utils/paging.js';
3
4
  export default class EphemeralStaticHostList extends BaseCommand {
4
5
  static args = {
5
6
  tenant_name: Args.string({
@@ -20,13 +21,17 @@ Available static hosts:
20
21
  - another-host (ID: 2)
21
22
  `,
22
23
  `$ xano ephemeral static_host list e4f2-9ab1-xyz1 -w 40 --output json
23
- [
24
- {
25
- "id": 1,
26
- "name": "my-static-host",
27
- "domain": "example.com"
28
- }
29
- ]
24
+ {
25
+ "count": 1,
26
+ "page": 1,
27
+ "total": 3,
28
+ "items": [
29
+ {
30
+ "id": 1,
31
+ "name": "my-static-host"
32
+ }
33
+ ]
34
+ }
30
35
  `,
31
36
  `$ xano ephemeral static_host list e4f2-9ab1-xyz1 -p staging -o json --page 2
32
37
  [
@@ -46,16 +51,7 @@ Available static hosts:
46
51
  options: ['summary', 'json'],
47
52
  required: false,
48
53
  }),
49
- page: Flags.integer({
50
- default: 1,
51
- description: 'Page number for pagination',
52
- required: false,
53
- }),
54
- per_page: Flags.integer({
55
- default: 50,
56
- description: 'Number of results per page',
57
- required: false,
58
- }),
54
+ ...pagingFlags('page-only-envelope', { fixedPerPage: 100 }),
59
55
  workspace: Flags.string({
60
56
  char: 'w',
61
57
  description: 'Workspace ID (optional if set in profile)',
@@ -80,13 +76,7 @@ Available static hosts:
80
76
  ` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
81
77
  }
82
78
  // Build query parameters
83
- const queryParams = new URLSearchParams({
84
- page: flags.page.toString(),
85
- });
86
- // Only add per_page if it's not the default value
87
- if (flags.per_page !== 50) {
88
- queryParams.append('per_page', flags.per_page.toString());
89
- }
79
+ const queryParams = new URLSearchParams(buildPagingParams(flags, 'page-only-envelope'));
90
80
  // Construct the API URL
91
81
  const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/static_host?${queryParams.toString()}`;
92
82
  // Fetch static hosts from the API
@@ -102,24 +92,11 @@ Available static hosts:
102
92
  const errorText = await response.text();
103
93
  this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
104
94
  }
105
- const data = await response.json();
106
- // Handle different response formats
107
- let staticHosts;
108
- if (Array.isArray(data)) {
109
- staticHosts = data;
110
- }
111
- else if (data && typeof data === 'object' && 'static_hosts' in data && Array.isArray(data.static_hosts)) {
112
- staticHosts = data.static_hosts;
113
- }
114
- else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
115
- staticHosts = data.items;
116
- }
117
- else {
118
- this.error('Unexpected API response format');
119
- }
95
+ const list = normalizeListResponse(await response.json(), ['static_hosts']);
96
+ const staticHosts = list.items;
120
97
  // Output results
121
98
  if (flags.output === 'json') {
122
- this.log(JSON.stringify(staticHosts, null, 2));
99
+ this.log(JSON.stringify(buildPagingJson(list, { page: flags.page, tier: 'page-only-envelope' }), null, 2));
123
100
  }
124
101
  else {
125
102
  // summary format
@@ -138,6 +115,9 @@ Available static hosts:
138
115
  }
139
116
  }
140
117
  }
118
+ const footer = formatPagingFooter(list, { noun: 'static host', page: flags.page, tier: 'page-only-envelope' });
119
+ if (footer)
120
+ this.log(footer);
141
121
  }
142
122
  }
143
123
  catch (error) {
@@ -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
+ }
@@ -4,14 +4,14 @@ export default class FunctionList extends BaseCommand {
4
4
  static description: string;
5
5
  static examples: string[];
6
6
  static flags: {
7
+ sort: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
+ workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
10
+ per_page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
7
11
  include_draft: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
12
  include_xanoscript: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
13
  order: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
14
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
- page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
12
- per_page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
13
- sort: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
14
- workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
15
  config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
16
  profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
17
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
@@ -1,5 +1,6 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../base-command.js';
3
+ import { buildPagingJson, formatPagingFooter, normalizeListResponse, pagingFlags } from '../../../utils/paging.js';
3
4
  export default class FunctionList extends BaseCommand {
4
5
  static args = {};
5
6
  static description = 'List all functions in a workspace from the Xano Metadata API';
@@ -16,20 +17,30 @@ Available functions:
16
17
  - another-function (ID: 2)
17
18
  `,
18
19
  `$ xano function:list -w 40 --output json
19
- [
20
- {
21
- "id": 1,
22
- "name": "function-1"
23
- }
24
- ]
20
+ {
21
+ "count": 1,
22
+ "page": 1,
23
+ "per_page": 50,
24
+ "next_page": 2,
25
+ "items": [
26
+ {
27
+ "id": 1,
28
+ "name": "function-1"
29
+ }
30
+ ]
31
+ }
25
32
  `,
26
33
  `$ xano function:list -p staging -o json --include_draft
27
- [
28
- {
29
- "id": 1,
30
- "name": "function-1"
31
- }
32
- ]
34
+ {
35
+ "count": 1,
36
+ "page": 1,
37
+ "items": [
38
+ {
39
+ "id": 1,
40
+ "name": "function-1"
41
+ }
42
+ ]
43
+ }
33
44
  `,
34
45
  ];
35
46
  static flags = {
@@ -57,16 +68,7 @@ Available functions:
57
68
  options: ['summary', 'json'],
58
69
  required: false,
59
70
  }),
60
- page: Flags.integer({
61
- default: 1,
62
- description: 'Page number for pagination',
63
- required: false,
64
- }),
65
- per_page: Flags.integer({
66
- default: 50,
67
- description: 'Number of results per page',
68
- required: false,
69
- }),
71
+ ...pagingFlags('envelope', { maxPerPage: 10_000 }),
70
72
  sort: Flags.string({
71
73
  default: 'created_at',
72
74
  description: 'Sort field',
@@ -118,24 +120,11 @@ Available functions:
118
120
  const errorText = await response.text();
119
121
  this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
120
122
  }
121
- const data = await response.json();
122
- // Handle different response formats
123
- let functions;
124
- if (Array.isArray(data)) {
125
- functions = data;
126
- }
127
- else if (data && typeof data === 'object' && 'functions' in data && Array.isArray(data.functions)) {
128
- functions = data.functions;
129
- }
130
- else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
131
- functions = data.items;
132
- }
133
- else {
134
- this.error('Unexpected API response format');
135
- }
123
+ const list = normalizeListResponse(await response.json(), ['functions']);
124
+ const functions = list.items;
136
125
  // Output results
137
126
  if (flags.output === 'json') {
138
- this.log(JSON.stringify(functions, null, 2));
127
+ this.log(JSON.stringify(buildPagingJson(list, { page: flags.page, perPage: flags.per_page, tier: 'envelope' }), null, 2));
139
128
  }
140
129
  else {
141
130
  // summary format
@@ -153,6 +142,9 @@ Available functions:
153
142
  }
154
143
  }
155
144
  }
145
+ const footer = formatPagingFooter(list, { noun: 'function', page: flags.page, tier: 'envelope' });
146
+ if (footer)
147
+ this.log(footer);
156
148
  }
157
149
  }
158
150
  catch (error) {
@@ -1,5 +1,6 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../base-command.js';
3
+ import { buildPagingJson } from '../../../utils/paging.js';
3
4
  export default class KnowledgeList extends BaseCommand {
4
5
  static args = {};
5
6
  static description = 'List workspace knowledge and skills as plain-text markdown';
@@ -92,7 +93,7 @@ export default class KnowledgeList extends BaseCommand {
92
93
  const data = await response.json();
93
94
  const items = Array.isArray(data) ? data : (data.items ?? []);
94
95
  if (flags.output === 'json') {
95
- this.log(JSON.stringify(items, null, 2));
96
+ this.log(JSON.stringify(buildPagingJson({ items }, { tier: 'none' }), null, 2));
96
97
  return;
97
98
  }
98
99
  this.log(this.formatMarkdown(items));
@@ -1,5 +1,6 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../base-command.js';
3
+ import { buildPagingJson } from '../../../utils/paging.js';
3
4
  export default class PlatformList extends BaseCommand {
4
5
  static description = 'List all platforms';
5
6
  static examples = [
@@ -47,22 +48,20 @@ Platforms:
47
48
  this.error('Unexpected API response format');
48
49
  }
49
50
  if (flags.output === 'json') {
50
- this.log(JSON.stringify(platforms, null, 2));
51
+ this.log(JSON.stringify(buildPagingJson({ items: platforms }, { tier: 'none' }), null, 2));
52
+ }
53
+ else if (platforms.length === 0) {
54
+ this.log('No platforms found');
51
55
  }
52
56
  else {
53
- if (platforms.length === 0) {
54
- this.log('No platforms found');
55
- }
56
- else {
57
- this.log('Platforms:');
58
- for (const platform of platforms) {
59
- const label = platform.name || `ID: ${platform.id}`;
60
- const helmTag = platform.helm?.tag ? ` | Helm: ${platform.helm.tag}` : '';
61
- const created = platform.created_at
62
- ? ` | Created: ${new Date(platform.created_at).toISOString().split('T')[0]}`
63
- : '';
64
- this.log(` ${label}${helmTag}${created}`);
65
- }
57
+ this.log('Platforms:');
58
+ for (const platform of platforms) {
59
+ const label = platform.name || `ID: ${platform.id}`;
60
+ const helmTag = platform.helm?.tag ? ` | Helm: ${platform.helm.tag}` : '';
61
+ const created = platform.created_at
62
+ ? ` | Created: ${new Date(platform.created_at).toISOString().split('T')[0]}`
63
+ : '';
64
+ this.log(` ${label}${helmTag}${created}`);
66
65
  }
67
66
  }
68
67
  }
@@ -1,5 +1,6 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../base-command.js';
3
+ import { buildPagingJson } from '../../../utils/paging.js';
3
4
  export default class ReleaseList extends BaseCommand {
4
5
  static description = 'List all releases in a workspace';
5
6
  static examples = [
@@ -57,22 +58,20 @@ Releases in workspace 5:
57
58
  this.error('Unexpected API response format');
58
59
  }
59
60
  if (flags.output === 'json') {
60
- this.log(JSON.stringify(releases, null, 2));
61
+ this.log(JSON.stringify(buildPagingJson({ items: releases }, { tier: 'none' }), null, 2));
62
+ }
63
+ else if (releases.length === 0) {
64
+ this.log('No releases found');
61
65
  }
62
66
  else {
63
- if (releases.length === 0) {
64
- this.log('No releases found');
65
- }
66
- else {
67
- this.log(`Releases in workspace ${workspaceId}:`);
68
- for (const release of releases) {
69
- const branch = release.branch ? ` - ${release.branch}` : '';
70
- const hotfix = release.hotfix ? ' [hotfix]' : '';
71
- const createdAt = release.created_at
72
- ? ` (${new Date(release.created_at).toLocaleString(undefined, { timeZoneName: 'short' })})`
73
- : '';
74
- this.log(` - ${release.name} (ID: ${release.id})${branch}${hotfix}${createdAt}`);
75
- }
67
+ this.log(`Releases in workspace ${workspaceId}:`);
68
+ for (const release of releases) {
69
+ const branch = release.branch ? ` - ${release.branch}` : '';
70
+ const hotfix = release.hotfix ? ' [hotfix]' : '';
71
+ const createdAt = release.created_at
72
+ ? ` (${new Date(release.created_at).toLocaleString(undefined, { timeZoneName: 'short' })})`
73
+ : '';
74
+ this.log(` - ${release.name} (ID: ${release.id})${branch}${hotfix}${createdAt}`);
76
75
  }
77
76
  }
78
77
  }
@@ -14,10 +14,4 @@ export default class ReleasePull extends BaseCommand {
14
14
  };
15
15
  run(): Promise<void>;
16
16
  private resolveReleaseName;
17
- /**
18
- * Sanitize a document name for use as a filename.
19
- * Strips quotes, replaces spaces with underscores, and removes
20
- * characters that are unsafe in filenames.
21
- */
22
- private sanitizeFilename;
23
17
  }
@@ -3,7 +3,7 @@ import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
4
  import snakeCase from 'lodash.snakecase';
5
5
  import BaseCommand from '../../../base-command.js';
6
- import { buildApiGroupFolderResolver, parseDocument } from '../../../utils/document-parser.js';
6
+ import { buildApiGroupFolderResolver, buildChannelServerResolver, parseDocument, resolveDocumentPath, } from '../../../utils/document-parser.js';
7
7
  export default class ReleasePull extends BaseCommand {
8
8
  static description = 'Pull a release multidoc from the Xano Metadata API and split into individual files';
9
9
  static examples = [
@@ -123,96 +123,19 @@ Pulled 58 documents from release 'v1.0'
123
123
  fs.mkdirSync(outputDir, { recursive: true });
124
124
  // Resolve api_group names to unique folder names, disambiguating collisions
125
125
  const getApiGroupFolder = buildApiGroupFolderResolver(documents, snakeCase);
126
+ // Resolve a realtime v2 channel path -> owning realtime_server name, so
127
+ // messages can nest under their channel's server (see resolver docs).
128
+ const getChannelServer = buildChannelServerResolver(documents);
126
129
  // Track filenames per type to handle duplicates
127
130
  const filenameCounters = new Map();
128
131
  let writtenCount = 0;
129
132
  for (const doc of documents) {
130
- let typeDir;
131
- let baseName;
132
- if (doc.type === 'workspace') {
133
- // workspace → workspace/{name}.xs
134
- typeDir = path.join(outputDir, 'workspace');
135
- baseName = this.sanitizeFilename(doc.name);
136
- }
137
- else if (doc.type === 'workspace_trigger') {
138
- // workspace_trigger → workspace/trigger/{name}.xs
139
- typeDir = path.join(outputDir, 'workspace', 'trigger');
140
- baseName = this.sanitizeFilename(doc.name);
141
- }
142
- else if (doc.type === 'error_trigger') {
143
- // error_trigger → workspace/trigger/{name}.xs (singleton, colocated with workspace triggers)
144
- typeDir = path.join(outputDir, 'workspace', 'trigger');
145
- baseName = this.sanitizeFilename(doc.name);
146
- }
147
- else if (doc.type === 'agent') {
148
- // agent → ai/agent/{name}.xs
149
- typeDir = path.join(outputDir, 'ai', 'agent');
150
- baseName = this.sanitizeFilename(doc.name);
151
- }
152
- else if (doc.type === 'mcp_server') {
153
- // mcp_server → ai/mcp_server/{name}.xs
154
- typeDir = path.join(outputDir, 'ai', 'mcp_server');
155
- baseName = this.sanitizeFilename(doc.name);
156
- }
157
- else if (doc.type === 'tool') {
158
- // tool → ai/tool/{name}.xs
159
- typeDir = path.join(outputDir, 'ai', 'tool');
160
- baseName = this.sanitizeFilename(doc.name);
161
- }
162
- else if (doc.type === 'agent_trigger') {
163
- // agent_trigger → ai/agent/trigger/{name}.xs
164
- typeDir = path.join(outputDir, 'ai', 'agent', 'trigger');
165
- baseName = this.sanitizeFilename(doc.name);
166
- }
167
- else if (doc.type === 'mcp_server_trigger') {
168
- // mcp_server_trigger → ai/mcp_server/trigger/{name}.xs
169
- typeDir = path.join(outputDir, 'ai', 'mcp_server', 'trigger');
170
- baseName = this.sanitizeFilename(doc.name);
171
- }
172
- else if (doc.type === 'table_trigger') {
173
- // table_trigger → table/trigger/{name}.xs
174
- typeDir = path.join(outputDir, 'table', 'trigger');
175
- baseName = this.sanitizeFilename(doc.name);
176
- }
177
- else if (doc.type === 'realtime_channel') {
178
- // realtime_channel → realtime/channel/{name}.xs
179
- typeDir = path.join(outputDir, 'realtime', 'channel');
180
- baseName = this.sanitizeFilename(doc.name);
181
- }
182
- else if (doc.type === 'realtime_trigger') {
183
- // realtime_trigger → realtime/trigger/{name}.xs
184
- typeDir = path.join(outputDir, 'realtime', 'trigger');
185
- baseName = this.sanitizeFilename(doc.name);
186
- }
187
- else if (doc.type === 'api_group') {
188
- // api_group "test" → api/{resolved_folder}/{name}.xs
189
- const groupFolder = getApiGroupFolder(doc.name);
190
- typeDir = path.join(outputDir, 'api', groupFolder);
191
- baseName = this.sanitizeFilename(doc.name);
192
- }
193
- else if (doc.type === 'query' && doc.apiGroup) {
194
- // query in group "test" → api/{resolved_folder}/{query_name}.xs
195
- const groupFolder = getApiGroupFolder(doc.apiGroup);
196
- const nameParts = doc.name.split('/');
197
- const leafName = nameParts.pop();
198
- const folderParts = nameParts.map((part) => snakeCase(part));
199
- typeDir = path.join(outputDir, 'api', groupFolder, ...folderParts);
200
- baseName = this.sanitizeFilename(leafName);
201
- if (doc.verb) {
202
- baseName = `${baseName}_${doc.verb}`;
203
- }
204
- }
205
- else {
206
- // Default: split folder path from name
207
- const nameParts = doc.name.split('/');
208
- const leafName = nameParts.pop();
209
- const folderParts = nameParts.map((part) => snakeCase(part));
210
- typeDir = path.join(outputDir, doc.type, ...folderParts);
211
- baseName = this.sanitizeFilename(leafName);
212
- if (doc.verb) {
213
- baseName = `${baseName}_${doc.verb}`;
214
- }
215
- }
133
+ const { baseName, typeDir } = resolveDocumentPath(doc, outputDir, {
134
+ getApiGroupFolder,
135
+ getChannelServer,
136
+ join: path.join,
137
+ snakeCase,
138
+ });
216
139
  fs.mkdirSync(typeDir, { recursive: true });
217
140
  // Track duplicates per directory
218
141
  const dirKey = path.relative(outputDir, typeDir);
@@ -257,12 +180,4 @@ Pulled 58 documents from release 'v1.0'
257
180
  }
258
181
  return match.id;
259
182
  }
260
- /**
261
- * Sanitize a document name for use as a filename.
262
- * Strips quotes, replaces spaces with underscores, and removes
263
- * characters that are unsafe in filenames.
264
- */
265
- sanitizeFilename(name) {
266
- return snakeCase(name.replaceAll('"', ''));
267
- }
268
183
  }
@@ -1,5 +1,6 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../../base-command.js';
3
+ import { formatPagingFooter } from '../../../../utils/paging.js';
3
4
  export default class SandboxEnvList extends BaseCommand {
4
5
  static description = 'List environment variable keys for a sandbox environment';
5
6
  static examples = [
@@ -51,6 +52,9 @@ Environment variables for sandbox environment:
51
52
  this.log(` - ${envVar.name}`);
52
53
  }
53
54
  }
55
+ const footer = formatPagingFooter({ items: envVars }, { noun: 'environment variable', tier: 'none' });
56
+ if (footer)
57
+ this.log(footer);
54
58
  }
55
59
  }
56
60
  catch (error) {