@xano/cli 1.0.6-beta.3 → 1.0.6-beta.6

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 (67) hide show
  1. package/README.md +95 -4
  2. package/dist/commands/branch/list/index.js +5 -1
  3. package/dist/commands/ephemeral/list/index.js +2 -1
  4. package/dist/commands/ephemeral/pull/index.d.ts +0 -6
  5. package/dist/commands/ephemeral/pull/index.js +10 -95
  6. package/dist/commands/ephemeral/static_host/build/list/index.d.ts +2 -3
  7. package/dist/commands/ephemeral/static_host/build/list/index.js +19 -40
  8. package/dist/commands/ephemeral/static_host/list/index.d.ts +2 -3
  9. package/dist/commands/ephemeral/static_host/list/index.js +20 -40
  10. package/dist/commands/function/list/index.d.ts +4 -4
  11. package/dist/commands/function/list/index.js +30 -38
  12. package/dist/commands/knowledge/list/index.js +2 -1
  13. package/dist/commands/platform/list/index.js +13 -14
  14. package/dist/commands/release/list/index.js +13 -14
  15. package/dist/commands/release/pull/index.d.ts +0 -6
  16. package/dist/commands/release/pull/index.js +10 -95
  17. package/dist/commands/sandbox/env/list/index.js +4 -0
  18. package/dist/commands/sandbox/pull/index.d.ts +0 -1
  19. package/dist/commands/sandbox/pull/index.js +10 -77
  20. package/dist/commands/sandbox/unit_test/list/index.d.ts +2 -0
  21. package/dist/commands/sandbox/unit_test/list/index.js +9 -14
  22. package/dist/commands/sandbox/unit_test/run_all/index.d.ts +1 -0
  23. package/dist/commands/sandbox/unit_test/run_all/index.js +67 -37
  24. package/dist/commands/sandbox/workflow_test/list/index.d.ts +2 -0
  25. package/dist/commands/sandbox/workflow_test/list/index.js +9 -14
  26. package/dist/commands/sandbox/workflow_test/run_all/index.d.ts +1 -0
  27. package/dist/commands/sandbox/workflow_test/run_all/index.js +65 -35
  28. package/dist/commands/static_host/build/list/index.d.ts +2 -3
  29. package/dist/commands/static_host/build/list/index.js +19 -40
  30. package/dist/commands/static_host/build/pull/index.js +12 -9
  31. package/dist/commands/static_host/list/index.d.ts +2 -3
  32. package/dist/commands/static_host/list/index.js +20 -40
  33. package/dist/commands/static_host/migrate/index.js +14 -10
  34. package/dist/commands/tenant/backup/list/index.js +13 -14
  35. package/dist/commands/tenant/cluster/list/index.js +9 -10
  36. package/dist/commands/tenant/env/list/index.js +4 -0
  37. package/dist/commands/tenant/list/index.js +19 -20
  38. package/dist/commands/tenant/pull/index.d.ts +0 -6
  39. package/dist/commands/tenant/pull/index.js +7 -142
  40. package/dist/commands/tenant/snapshot/list/index.js +5 -1
  41. package/dist/commands/tenant/unit_test/list/index.d.ts +4 -2
  42. package/dist/commands/tenant/unit_test/list/index.js +9 -14
  43. package/dist/commands/tenant/unit_test/run_all/index.d.ts +1 -0
  44. package/dist/commands/tenant/unit_test/run_all/index.js +67 -37
  45. package/dist/commands/tenant/workflow_test/list/index.d.ts +4 -2
  46. package/dist/commands/tenant/workflow_test/list/index.js +9 -14
  47. package/dist/commands/tenant/workflow_test/run_all/index.d.ts +1 -0
  48. package/dist/commands/tenant/workflow_test/run_all/index.js +65 -35
  49. package/dist/commands/unit_test/list/index.d.ts +3 -1
  50. package/dist/commands/unit_test/list/index.js +9 -14
  51. package/dist/commands/unit_test/run_all/index.d.ts +1 -0
  52. package/dist/commands/unit_test/run_all/index.js +68 -40
  53. package/dist/commands/workflow_test/list/index.d.ts +3 -1
  54. package/dist/commands/workflow_test/list/index.js +10 -13
  55. package/dist/commands/workflow_test/run_all/index.d.ts +1 -0
  56. package/dist/commands/workflow_test/run_all/index.js +67 -36
  57. package/dist/commands/workspace/list/index.js +7 -13
  58. package/dist/commands/workspace/pull/index.d.ts +0 -6
  59. package/dist/commands/workspace/pull/index.js +7 -144
  60. package/dist/utils/concurrency.d.ts +29 -0
  61. package/dist/utils/concurrency.js +54 -0
  62. package/dist/utils/document-parser.d.ts +39 -0
  63. package/dist/utils/document-parser.js +139 -0
  64. package/dist/utils/paging.d.ts +141 -0
  65. package/dist/utils/paging.js +194 -0
  66. package/oclif.manifest.json +2950 -2824
  67. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { 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 StaticHostList extends BaseCommand {
4
5
  static args = {};
5
6
  static description = 'List all static hosts in a workspace from the Xano Metadata API';
@@ -15,13 +16,17 @@ Available static hosts:
15
16
  - another-host (ID: 2)
16
17
  `,
17
18
  `$ xano static_host:list -w 40 --output json
18
- [
19
- {
20
- "id": 1,
21
- "name": "my-static-host",
22
- "domain": "example.com"
23
- }
24
- ]
19
+ {
20
+ "count": 1,
21
+ "page": 1,
22
+ "total": 3,
23
+ "items": [
24
+ {
25
+ "id": 1,
26
+ "name": "my-static-host"
27
+ }
28
+ ]
29
+ }
25
30
  `,
26
31
  `$ xano static_host:list -p staging -o json --page 2
27
32
  [
@@ -41,16 +46,7 @@ Available static hosts:
41
46
  options: ['summary', 'json'],
42
47
  required: false,
43
48
  }),
44
- page: Flags.integer({
45
- default: 1,
46
- description: 'Page number for pagination',
47
- required: false,
48
- }),
49
- per_page: Flags.integer({
50
- default: 50,
51
- description: 'Number of results per page',
52
- required: false,
53
- }),
49
+ ...pagingFlags('page-only-envelope', { fixedPerPage: 100 }),
54
50
  workspace: Flags.string({
55
51
  char: 'w',
56
52
  description: 'Workspace ID (optional if set in profile)',
@@ -74,13 +70,7 @@ Available static hosts:
74
70
  ` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
75
71
  }
76
72
  // Build query parameters
77
- const queryParams = new URLSearchParams({
78
- page: flags.page.toString(),
79
- });
80
- // Only add per_page if it's not the default value
81
- if (flags.per_page !== 50) {
82
- queryParams.append('per_page', flags.per_page.toString());
83
- }
73
+ const queryParams = new URLSearchParams(buildPagingParams(flags, 'page-only-envelope'));
84
74
  // Construct the API URL
85
75
  const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host?${queryParams.toString()}`;
86
76
  // Fetch static hosts from the API
@@ -96,24 +86,11 @@ Available static hosts:
96
86
  const errorText = await response.text();
97
87
  this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
98
88
  }
99
- const data = await response.json();
100
- // Handle different response formats
101
- let staticHosts;
102
- if (Array.isArray(data)) {
103
- staticHosts = data;
104
- }
105
- else if (data && typeof data === 'object' && 'static_hosts' in data && Array.isArray(data.static_hosts)) {
106
- staticHosts = data.static_hosts;
107
- }
108
- else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
109
- staticHosts = data.items;
110
- }
111
- else {
112
- this.error('Unexpected API response format');
113
- }
89
+ const list = normalizeListResponse(await response.json(), ['static_hosts']);
90
+ const staticHosts = list.items;
114
91
  // Output results
115
92
  if (flags.output === 'json') {
116
- this.log(JSON.stringify(staticHosts, null, 2));
93
+ this.log(JSON.stringify(buildPagingJson(list, { page: flags.page, tier: 'page-only-envelope' }), null, 2));
117
94
  }
118
95
  else {
119
96
  // summary format
@@ -132,6 +109,9 @@ Available static hosts:
132
109
  }
133
110
  }
134
111
  }
112
+ const footer = formatPagingFooter(list, { noun: 'static host', page: flags.page, tier: 'page-only-envelope' });
113
+ if (footer)
114
+ this.log(footer);
135
115
  }
136
116
  }
137
117
  catch (error) {
@@ -1,5 +1,6 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../base-command.js';
3
+ import { normalizeListResponse } from '../../../utils/paging.js';
3
4
  export default class StaticHostMigrate extends BaseCommand {
4
5
  static args = {
5
6
  static_host: Args.string({
@@ -85,9 +86,12 @@ Would migrate 3 v1 host(s): newsite, marketing, legacy
85
86
  /** List all static hosts in the workspace (paginates until exhausted). */
86
87
  async listHosts(profile, workspaceId, verbose) {
87
88
  const hosts = [];
88
- const perPage = 100;
89
89
  for (let page = 1;; page++) {
90
- const listUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host?page=${page}&per_page=${perPage}`;
90
+ // per_page is not an input on this endpoint — the server fixes the page
91
+ // size — so it is deliberately not sent. Termination uses the response's
92
+ // own itemsTotal rather than "the page came back full", which would skip
93
+ // hosts if the server page size ever differed from a hardcoded guess.
94
+ const listUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host?page=${page}`;
91
95
  // eslint-disable-next-line no-await-in-loop
92
96
  const response = await this.verboseFetch(listUrl, {
93
97
  headers: {
@@ -100,14 +104,14 @@ Would migrate 3 v1 host(s): newsite, marketing, legacy
100
104
  const errorText = await response.text(); // eslint-disable-line no-await-in-loop
101
105
  this.error(`Failed to list static hosts: ${response.status} ${response.statusText}\n${errorText}`);
102
106
  }
103
- const data = (await response.json()); // eslint-disable-line no-await-in-loop
104
- const pageHosts = Array.isArray(data)
105
- ? data
106
- : data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)
107
- ? data.items
108
- : [];
109
- hosts.push(...pageHosts);
110
- if (pageHosts.length < perPage)
107
+ const list = normalizeListResponse(await response.json(), ['static_hosts']); // eslint-disable-line no-await-in-loop
108
+ hosts.push(...list.items);
109
+ // Stop on the server's own total when it gives us one. Fall back to an
110
+ // empty page otherwise — one extra request, but it cannot under-collect
111
+ // and silently migrate a subset.
112
+ if (list.items.length === 0)
113
+ break;
114
+ if (list.itemsTotal !== undefined && hosts.length >= list.itemsTotal)
111
115
  break;
112
116
  }
113
117
  return hosts;
@@ -1,5 +1,6 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../../base-command.js';
3
+ import { buildPagingJson } from '../../../../utils/paging.js';
3
4
  export default class TenantBackupList extends BaseCommand {
4
5
  static args = {
5
6
  tenant_name: Args.string({
@@ -70,23 +71,21 @@ Backups for tenant t1234-abcd-xyz1:
70
71
  this.error('Unexpected API response format');
71
72
  }
72
73
  if (flags.output === 'json') {
73
- this.log(JSON.stringify(backups, null, 2));
74
+ this.log(JSON.stringify(buildPagingJson({ items: backups }, { tier: 'none' }), null, 2));
75
+ }
76
+ else if (backups.length === 0) {
77
+ this.log(`No backups found for tenant ${tenantName}`);
74
78
  }
75
79
  else {
76
- if (backups.length === 0) {
77
- this.log(`No backups found for tenant ${tenantName}`);
78
- }
79
- else {
80
- this.log(`Backups for tenant ${tenantName}:`);
81
- for (const backup of backups) {
82
- let date = 'unknown';
83
- if (backup.created_at) {
84
- const d = new Date(backup.created_at);
85
- date = Number.isNaN(d.getTime()) ? backup.created_at : d.toISOString().split('T')[0];
86
- }
87
- const desc = backup.description || 'No description';
88
- this.log(` - #${backup.id} - ${desc} (${date})`);
80
+ this.log(`Backups for tenant ${tenantName}:`);
81
+ for (const backup of backups) {
82
+ let date = 'unknown';
83
+ if (backup.created_at) {
84
+ const d = new Date(backup.created_at);
85
+ date = Number.isNaN(d.getTime()) ? backup.created_at : d.toISOString().split('T')[0];
89
86
  }
87
+ const desc = backup.description || 'No description';
88
+ this.log(` - #${backup.id} - ${desc} (${date})`);
90
89
  }
91
90
  }
92
91
  }
@@ -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 TenantClusterList extends BaseCommand {
4
5
  static description = 'List all tenant clusters';
5
6
  static examples = [
@@ -48,18 +49,16 @@ Tenant clusters:
48
49
  this.error('Unexpected API response format');
49
50
  }
50
51
  if (flags.output === 'json') {
51
- this.log(JSON.stringify(clusters, null, 2));
52
+ this.log(JSON.stringify(buildPagingJson({ items: clusters }, { tier: 'none' }), null, 2));
53
+ }
54
+ else if (clusters.length === 0) {
55
+ this.log('No tenant clusters found');
52
56
  }
53
57
  else {
54
- if (clusters.length === 0) {
55
- this.log('No tenant clusters found');
56
- }
57
- else {
58
- this.log('Tenant clusters:');
59
- for (const cluster of clusters) {
60
- const type = cluster.type ? ` (${cluster.type})` : '';
61
- this.log(` - ${cluster.name}${type} [ID: ${cluster.id}]`);
62
- }
58
+ this.log('Tenant clusters:');
59
+ for (const cluster of clusters) {
60
+ const type = cluster.type ? ` (${cluster.type})` : '';
61
+ this.log(` - ${cluster.name}${type} [ID: ${cluster.id}]`);
63
62
  }
64
63
  }
65
64
  }
@@ -1,5 +1,6 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../../base-command.js';
3
+ import { formatPagingFooter } from '../../../../utils/paging.js';
3
4
  export default class TenantEnvList extends BaseCommand {
4
5
  static args = {
5
6
  tenant_name: Args.string({
@@ -68,6 +69,9 @@ Environment variables for tenant my-tenant:
68
69
  this.log(` - ${envVar.name}`);
69
70
  }
70
71
  }
72
+ const footer = formatPagingFooter({ items: envVars }, { noun: 'environment variable', tier: 'none' });
73
+ if (footer)
74
+ this.log(footer);
71
75
  }
72
76
  }
73
77
  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 TenantList extends BaseCommand {
4
5
  static description = 'List all tenants in a workspace';
5
6
  static examples = [
@@ -65,28 +66,26 @@ Tenants in workspace 5:
65
66
  this.error('Unexpected API response format');
66
67
  }
67
68
  if (flags.output === 'json') {
68
- this.log(JSON.stringify(tenants, null, 2));
69
+ this.log(JSON.stringify(buildPagingJson({ items: tenants }, { tier: 'none' }), null, 2));
70
+ }
71
+ else if (tenants.length === 0) {
72
+ this.log('No tenants found');
69
73
  }
70
74
  else {
71
- if (tenants.length === 0) {
72
- this.log('No tenants found');
73
- }
74
- else {
75
- this.log(`Tenants in workspace ${workspaceId}:`);
76
- for (const tenant of tenants) {
77
- const state = tenant.state ? ` [${tenant.state}]` : '';
78
- const license = tenant.license ? ` - ${tenant.license}` : '';
79
- const typeLabel = tenant.type && tenant.type !== 'standard' ? ` [${tenant.type}]` : '';
80
- this.log(` - ${tenant.display || tenant.name} (${tenant.name})${state}${license}${typeLabel}`);
81
- if (tenant.cluster?.name)
82
- this.log(` Cluster: ${tenant.cluster.name}`);
83
- const releaseName = typeof tenant.release === 'string' ? tenant.release : tenant.release?.name;
84
- const releaseId = typeof tenant.release === 'object' ? tenant.release?.id : undefined;
85
- if (releaseName)
86
- this.log(` Release: ${releaseName} (ID: ${releaseId})`);
87
- if (tenant.platform?.name)
88
- this.log(` Platform: ${tenant.platform.name}`);
89
- }
75
+ this.log(`Tenants in workspace ${workspaceId}:`);
76
+ for (const tenant of tenants) {
77
+ const state = tenant.state ? ` [${tenant.state}]` : '';
78
+ const license = tenant.license ? ` - ${tenant.license}` : '';
79
+ const typeLabel = tenant.type && tenant.type !== 'standard' ? ` [${tenant.type}]` : '';
80
+ this.log(` - ${tenant.display || tenant.name} (${tenant.name})${state}${license}${typeLabel}`);
81
+ if (tenant.cluster?.name)
82
+ this.log(` Cluster: ${tenant.cluster.name}`);
83
+ const releaseName = typeof tenant.release === 'string' ? tenant.release : tenant.release?.name;
84
+ const releaseId = typeof tenant.release === 'object' ? tenant.release?.id : undefined;
85
+ if (releaseName)
86
+ this.log(` Release: ${releaseName} (ID: ${releaseId})`);
87
+ if (tenant.platform?.name)
88
+ this.log(` Platform: ${tenant.platform.name}`);
90
89
  }
91
90
  }
92
91
  }
@@ -14,10 +14,4 @@ export default class Pull extends BaseCommand {
14
14
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
15
  };
16
16
  run(): Promise<void>;
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, buildChannelServerResolver, channelPathSegments, parseDocument, } from '../../../utils/document-parser.js';
6
+ import { buildApiGroupFolderResolver, buildChannelServerResolver, parseDocument, resolveDocumentPath, } from '../../../utils/document-parser.js';
7
7
  export default class Pull extends BaseCommand {
8
8
  static description = 'Pull a tenant multidoc from the Xano Metadata API and split into individual files';
9
9
  static examples = [
@@ -136,139 +136,12 @@ Pulled 58 documents from tenant my-tenant
136
136
  const filenameCounters = new Map();
137
137
  let writtenCount = 0;
138
138
  for (const doc of documents) {
139
- let typeDir;
140
- let baseName;
141
- if (doc.type === 'workspace') {
142
- // workspace → workspace/{name}.xs
143
- typeDir = path.join(outputDir, 'workspace');
144
- baseName = this.sanitizeFilename(doc.name);
145
- }
146
- else if (doc.type === 'workspace_trigger') {
147
- // workspace_trigger → workspace/trigger/{name}.xs
148
- typeDir = path.join(outputDir, 'workspace', 'trigger');
149
- baseName = this.sanitizeFilename(doc.name);
150
- }
151
- else if (doc.type === 'error_trigger') {
152
- // error_trigger → workspace/trigger/{name}.xs (singleton, colocated with workspace triggers)
153
- typeDir = path.join(outputDir, 'workspace', 'trigger');
154
- baseName = this.sanitizeFilename(doc.name);
155
- }
156
- else if (doc.type === 'agent') {
157
- // agent → ai/agent/{name}.xs
158
- typeDir = path.join(outputDir, 'ai', 'agent');
159
- baseName = this.sanitizeFilename(doc.name);
160
- }
161
- else if (doc.type === 'mcp_server') {
162
- // mcp_server → ai/mcp_server/{name}.xs
163
- typeDir = path.join(outputDir, 'ai', 'mcp_server');
164
- baseName = this.sanitizeFilename(doc.name);
165
- }
166
- else if (doc.type === 'tool') {
167
- // tool → ai/tool/{name}.xs
168
- typeDir = path.join(outputDir, 'ai', 'tool');
169
- baseName = this.sanitizeFilename(doc.name);
170
- }
171
- else if (doc.type === 'agent_trigger') {
172
- // agent_trigger → ai/agent/trigger/{name}.xs
173
- typeDir = path.join(outputDir, 'ai', 'agent', 'trigger');
174
- baseName = this.sanitizeFilename(doc.name);
175
- }
176
- else if (doc.type === 'mcp_server_trigger') {
177
- // mcp_server_trigger → ai/mcp_server/trigger/{name}.xs
178
- typeDir = path.join(outputDir, 'ai', 'mcp_server', 'trigger');
179
- baseName = this.sanitizeFilename(doc.name);
180
- }
181
- else if (doc.type === 'table_trigger') {
182
- // table_trigger → table/trigger/{name}.xs
183
- typeDir = path.join(outputDir, 'table', 'trigger');
184
- baseName = this.sanitizeFilename(doc.name);
185
- }
186
- else if (doc.type === 'realtime_channel') {
187
- // realtime_channel → realtime/channel/{name}.xs
188
- typeDir = path.join(outputDir, 'realtime', 'channel');
189
- baseName = this.sanitizeFilename(doc.name);
190
- }
191
- else if (doc.type === 'realtime_trigger') {
192
- // realtime_trigger → realtime/trigger/{name}.xs
193
- typeDir = path.join(outputDir, 'realtime', 'trigger');
194
- baseName = this.sanitizeFilename(doc.name);
195
- }
196
- else if (doc.type === 'realtime_server') {
197
- // Realtime v2 — see workspace/pull for the full rationale. Its own
198
- // document is named after itself, mirroring api_group.
199
- // realtime_server "chat" → realtime/server/chat/chat.xs
200
- typeDir = path.join(outputDir, 'realtime', 'server', this.sanitizeFilename(doc.name));
201
- baseName = this.sanitizeFilename(doc.name);
202
- }
203
- else if (doc.type === 'channel') {
204
- // Realtime v2. The channel owns a directory named after itself (the full
205
- // channel name, snake_cased into a single flat segment — like every other
206
- // object), and its messages live in a message/ subfolder inside it. It
207
- // nests under its owning realtime_server (from `server = "..."`):
208
- // channel "rooms/{room_id}" (server "chat")
209
- // → realtime/server/chat/channel/rooms_room_id/rooms_room_id.xs
210
- //
211
- // The channel name is snake_cased as a whole (rooms/{room_id} →
212
- // rooms_room_id), so there is no path nesting and no bracket escaping —
213
- // the on-disk name is a display form; the .xs content keeps the real
214
- // name. A channel with no resolvable server (e.g. a pre-server export)
215
- // falls back to the legacy flat channel/<path>/_channel.xs layout.
216
- if (doc.server) {
217
- typeDir = path.join(outputDir, 'realtime', 'server', this.sanitizeFilename(doc.server), 'channel', snakeCase(doc.name));
218
- baseName = snakeCase(doc.name);
219
- }
220
- else {
221
- typeDir = path.join(outputDir, 'channel', ...channelPathSegments(doc.name, snakeCase));
222
- baseName = '_channel';
223
- }
224
- }
225
- else if (doc.type === 'message' && doc.channel) {
226
- // Realtime v2 message → nests in a message/ subfolder under its channel,
227
- // under that channel's server. The message names only its channel; the
228
- // server is resolved by looking that channel up in the same multidoc
229
- // (two-pass resolve).
230
- // message "post" on channel "rooms/{room_id}" (server "chat")
231
- // → realtime/server/chat/channel/rooms_room_id/message/post.xs
232
- //
233
- // Nesting matters beyond tidiness: message names are unique only WITHIN
234
- // a channel, so a flat message/ directory would collide when two
235
- // channels both define e.g. "say". A channel whose server can't be
236
- // resolved falls back to the legacy flat channel/ layout.
237
- const messageServer = getChannelServer(doc.channel);
238
- typeDir = messageServer
239
- ? path.join(outputDir, 'realtime', 'server', this.sanitizeFilename(messageServer), 'channel', snakeCase(doc.channel), 'message')
240
- : path.join(outputDir, 'channel', ...channelPathSegments(doc.channel, snakeCase));
241
- baseName = this.sanitizeFilename(doc.name);
242
- }
243
- else if (doc.type === 'api_group') {
244
- // api_group "test" → api/{resolved_folder}/{name}.xs
245
- const groupFolder = getApiGroupFolder(doc.name);
246
- typeDir = path.join(outputDir, 'api', groupFolder);
247
- baseName = this.sanitizeFilename(doc.name);
248
- }
249
- else if (doc.type === 'query' && doc.apiGroup) {
250
- // query in group "test" → api/{resolved_folder}/{query_name}.xs
251
- const groupFolder = getApiGroupFolder(doc.apiGroup);
252
- const nameParts = doc.name.split('/');
253
- const leafName = nameParts.pop();
254
- const folderParts = nameParts.map((part) => snakeCase(part));
255
- typeDir = path.join(outputDir, 'api', groupFolder, ...folderParts);
256
- baseName = this.sanitizeFilename(leafName);
257
- if (doc.verb) {
258
- baseName = `${baseName}_${doc.verb}`;
259
- }
260
- }
261
- else {
262
- // Default: split folder path from name
263
- const nameParts = doc.name.split('/');
264
- const leafName = nameParts.pop();
265
- const folderParts = nameParts.map((part) => snakeCase(part));
266
- typeDir = path.join(outputDir, doc.type, ...folderParts);
267
- baseName = this.sanitizeFilename(leafName);
268
- if (doc.verb) {
269
- baseName = `${baseName}_${doc.verb}`;
270
- }
271
- }
139
+ const { baseName, typeDir } = resolveDocumentPath(doc, outputDir, {
140
+ getApiGroupFolder,
141
+ getChannelServer,
142
+ join: path.join,
143
+ snakeCase,
144
+ });
272
145
  fs.mkdirSync(typeDir, { recursive: true });
273
146
  // Track duplicates per directory
274
147
  const dirKey = path.relative(outputDir, typeDir);
@@ -287,12 +160,4 @@ Pulled 58 documents from tenant my-tenant
287
160
  }
288
161
  this.log(`Pulled ${writtenCount} documents from tenant ${tenantName} to ${flags.directory}`);
289
162
  }
290
- /**
291
- * Sanitize a document name for use as a filename.
292
- * Strips quotes, replaces spaces with underscores, and removes
293
- * characters that are unsafe in filenames.
294
- */
295
- sanitizeFilename(name) {
296
- return snakeCase(name.replaceAll('"', ''));
297
- }
298
163
  }
@@ -1,5 +1,6 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../../base-command.js';
3
+ import { buildPagingJson, formatPagingFooter } from '../../../../utils/paging.js';
3
4
  export default class TenantSnapshotList extends BaseCommand {
4
5
  static args = {
5
6
  tenant_name: Args.string({
@@ -64,7 +65,7 @@ Snapshots for tenant t1234-abcd-xyz1:
64
65
  this.error('Unexpected API response format');
65
66
  }
66
67
  if (flags.output === 'json') {
67
- this.log(JSON.stringify(snapshots, null, 2));
68
+ this.log(JSON.stringify(buildPagingJson({ items: snapshots }, { tier: 'none' }), null, 2));
68
69
  }
69
70
  else if (snapshots.length === 0) {
70
71
  this.log(`No snapshots found for tenant ${tenantName}`);
@@ -82,6 +83,9 @@ Snapshots for tenant t1234-abcd-xyz1:
82
83
  const tagStr = tags.length > 0 ? ` [${tags.join(', ')}]` : '';
83
84
  this.log(` - ${snapshot.name} (${meta})${tagStr}`);
84
85
  }
86
+ const footer = formatPagingFooter({ items: snapshots }, { noun: 'snapshot', tier: 'none' });
87
+ if (footer)
88
+ this.log(footer);
85
89
  }
86
90
  }
87
91
  catch (error) {
@@ -3,11 +3,13 @@ export default class TenantUnitTestList extends BaseCommand {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static flags: {
6
+ tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
9
+ per_page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
6
10
  branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
11
  'obj-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
12
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
- tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
- workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
13
  config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
14
  profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
15
  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, buildPagingParams, formatPagingFooter, normalizeListResponse, pagingFlags } from '../../../../utils/paging.js';
3
4
  export default class TenantUnitTestList extends BaseCommand {
4
5
  static description = 'List all unit tests for a tenant';
5
6
  static examples = [
@@ -28,6 +29,7 @@ Unit tests for tenant my-tenant:
28
29
  options: ['summary', 'json'],
29
30
  required: false,
30
31
  }),
32
+ ...pagingFlags('envelope', { maxPerPage: 10_000 }),
31
33
  tenant: Flags.string({
32
34
  char: 't',
33
35
  description: 'Tenant name',
@@ -47,8 +49,7 @@ Unit tests for tenant my-tenant:
47
49
  this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
48
50
  }
49
51
  const tenantName = encodeURIComponent(flags.tenant);
50
- const params = new URLSearchParams();
51
- params.set('per_page', '10000');
52
+ const params = new URLSearchParams(buildPagingParams(flags, 'envelope'));
52
53
  if (flags.branch)
53
54
  params.set('branch', flags.branch);
54
55
  if (flags['obj-type'])
@@ -66,19 +67,10 @@ Unit tests for tenant my-tenant:
66
67
  const errorText = await response.text();
67
68
  this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
68
69
  }
69
- const data = (await response.json());
70
- let tests;
71
- if (Array.isArray(data)) {
72
- tests = data;
73
- }
74
- else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
75
- tests = data.items;
76
- }
77
- else {
78
- this.error('Unexpected API response format');
79
- }
70
+ const list = normalizeListResponse(await response.json());
71
+ const tests = list.items;
80
72
  if (flags.output === 'json') {
81
- this.log(JSON.stringify(tests, null, 2));
73
+ this.log(JSON.stringify(buildPagingJson(list, { page: flags.page, perPage: flags.per_page, tier: 'envelope' }), null, 2));
82
74
  }
83
75
  else {
84
76
  if (tests.length === 0) {
@@ -90,6 +82,9 @@ Unit tests for tenant my-tenant:
90
82
  this.log(` - ${test.name} (ID: ${test.id}) [${test.obj_type}: ${test.obj_name}]`);
91
83
  }
92
84
  }
85
+ const footer = formatPagingFooter(list, { noun: 'unit test', page: flags.page, tier: 'envelope' });
86
+ if (footer)
87
+ this.log(footer);
93
88
  }
94
89
  }
95
90
  catch (error) {
@@ -4,6 +4,7 @@ export default class TenantUnitTestRunAll extends BaseCommand {
4
4
  static examples: string[];
5
5
  static flags: {
6
6
  branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ concurrency: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
7
8
  'obj-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
9
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
10
  tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;