@xano/cli 1.0.6-beta.0 → 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 (101) hide show
  1. package/README.md +124 -7
  2. package/dist/base-command.d.ts +2 -0
  3. package/dist/base-command.js +10 -4
  4. package/dist/commands/branch/list/index.js +5 -1
  5. package/dist/commands/ephemeral/impersonate/index.d.ts +1 -0
  6. package/dist/commands/ephemeral/impersonate/index.js +11 -3
  7. package/dist/commands/ephemeral/list/index.js +2 -1
  8. package/dist/commands/ephemeral/pull/index.d.ts +3 -7
  9. package/dist/commands/ephemeral/pull/index.js +25 -109
  10. package/dist/commands/ephemeral/push/index.d.ts +13 -1
  11. package/dist/commands/ephemeral/push/index.js +94 -16
  12. package/dist/commands/ephemeral/static_host/build/delete/index.d.ts +20 -0
  13. package/dist/commands/ephemeral/static_host/build/delete/index.js +121 -0
  14. package/dist/commands/ephemeral/static_host/build/get/index.d.ts +18 -0
  15. package/dist/commands/ephemeral/static_host/build/get/index.js +137 -0
  16. package/dist/commands/ephemeral/static_host/build/list/index.d.ts +18 -0
  17. package/dist/commands/ephemeral/static_host/build/list/index.js +137 -0
  18. package/dist/commands/ephemeral/static_host/build/pull/index.d.ts +53 -0
  19. package/dist/commands/ephemeral/static_host/build/pull/index.js +308 -0
  20. package/dist/commands/ephemeral/static_host/build/push/index.d.ts +24 -0
  21. package/dist/commands/ephemeral/static_host/build/push/index.js +250 -0
  22. package/dist/commands/ephemeral/static_host/create/index.d.ts +18 -0
  23. package/dist/commands/ephemeral/static_host/create/index.js +92 -0
  24. package/dist/commands/ephemeral/static_host/deploy/index.d.ts +19 -0
  25. package/dist/commands/ephemeral/static_host/deploy/index.js +112 -0
  26. package/dist/commands/ephemeral/static_host/edit/index.d.ts +24 -0
  27. package/dist/commands/ephemeral/static_host/edit/index.js +158 -0
  28. package/dist/commands/ephemeral/static_host/get/index.d.ts +19 -0
  29. package/dist/commands/ephemeral/static_host/get/index.js +101 -0
  30. package/dist/commands/ephemeral/static_host/list/index.d.ts +17 -0
  31. package/dist/commands/ephemeral/static_host/list/index.js +132 -0
  32. package/dist/commands/flatten/index.d.ts +34 -0
  33. package/dist/commands/flatten/index.js +140 -0
  34. package/dist/commands/function/list/index.d.ts +4 -4
  35. package/dist/commands/function/list/index.js +30 -38
  36. package/dist/commands/knowledge/list/index.js +2 -1
  37. package/dist/commands/platform/list/index.js +13 -14
  38. package/dist/commands/release/list/index.js +13 -14
  39. package/dist/commands/release/pull/index.d.ts +0 -6
  40. package/dist/commands/release/pull/index.js +10 -95
  41. package/dist/commands/sandbox/env/list/index.js +4 -0
  42. package/dist/commands/sandbox/pull/index.d.ts +0 -1
  43. package/dist/commands/sandbox/pull/index.js +10 -77
  44. package/dist/commands/sandbox/unit_test/list/index.d.ts +2 -0
  45. package/dist/commands/sandbox/unit_test/list/index.js +9 -14
  46. package/dist/commands/sandbox/unit_test/run_all/index.d.ts +1 -0
  47. package/dist/commands/sandbox/unit_test/run_all/index.js +67 -37
  48. package/dist/commands/sandbox/workflow_test/list/index.d.ts +2 -0
  49. package/dist/commands/sandbox/workflow_test/list/index.js +9 -14
  50. package/dist/commands/sandbox/workflow_test/run_all/index.d.ts +1 -0
  51. package/dist/commands/sandbox/workflow_test/run_all/index.js +65 -35
  52. package/dist/commands/static_host/build/list/index.d.ts +2 -3
  53. package/dist/commands/static_host/build/list/index.js +19 -40
  54. package/dist/commands/static_host/build/pull/index.js +12 -9
  55. package/dist/commands/static_host/list/index.d.ts +2 -3
  56. package/dist/commands/static_host/list/index.js +20 -40
  57. package/dist/commands/static_host/migrate/index.js +14 -10
  58. package/dist/commands/tenant/backup/list/index.js +13 -14
  59. package/dist/commands/tenant/cluster/list/index.js +9 -10
  60. package/dist/commands/tenant/env/list/index.js +4 -0
  61. package/dist/commands/tenant/list/index.js +19 -20
  62. package/dist/commands/tenant/pull/index.d.ts +0 -6
  63. package/dist/commands/tenant/pull/index.js +10 -95
  64. package/dist/commands/tenant/snapshot/list/index.js +5 -1
  65. package/dist/commands/tenant/unit_test/list/index.d.ts +4 -2
  66. package/dist/commands/tenant/unit_test/list/index.js +9 -14
  67. package/dist/commands/tenant/unit_test/run_all/index.d.ts +1 -0
  68. package/dist/commands/tenant/unit_test/run_all/index.js +67 -37
  69. package/dist/commands/tenant/workflow_test/list/index.d.ts +4 -2
  70. package/dist/commands/tenant/workflow_test/list/index.js +9 -14
  71. package/dist/commands/tenant/workflow_test/run_all/index.d.ts +1 -0
  72. package/dist/commands/tenant/workflow_test/run_all/index.js +65 -35
  73. package/dist/commands/unit_test/list/index.d.ts +3 -1
  74. package/dist/commands/unit_test/list/index.js +9 -14
  75. package/dist/commands/unit_test/run_all/index.d.ts +1 -0
  76. package/dist/commands/unit_test/run_all/index.js +68 -40
  77. package/dist/commands/workflow_test/list/index.d.ts +3 -1
  78. package/dist/commands/workflow_test/list/index.js +10 -13
  79. package/dist/commands/workflow_test/run_all/index.d.ts +1 -0
  80. package/dist/commands/workflow_test/run_all/index.js +67 -36
  81. package/dist/commands/workspace/git/pull/index.js +53 -3
  82. package/dist/commands/workspace/list/index.js +7 -13
  83. package/dist/commands/workspace/pull/index.d.ts +0 -6
  84. package/dist/commands/workspace/pull/index.js +10 -95
  85. package/dist/utils/concurrency.d.ts +29 -0
  86. package/dist/utils/concurrency.js +54 -0
  87. package/dist/utils/document-parser.d.ts +116 -0
  88. package/dist/utils/document-parser.js +274 -1
  89. package/dist/utils/flatten.d.ts +36 -0
  90. package/dist/utils/flatten.js +68 -0
  91. package/dist/utils/knowledge-sync.d.ts +1 -0
  92. package/dist/utils/knowledge-sync.js +11 -5
  93. package/dist/utils/microservice-wait.d.ts +53 -0
  94. package/dist/utils/microservice-wait.js +83 -0
  95. package/dist/utils/multidoc-push.d.ts +12 -0
  96. package/dist/utils/multidoc-push.js +84 -0
  97. package/dist/utils/paging.d.ts +145 -0
  98. package/dist/utils/paging.js +199 -0
  99. package/dist/utils/reference-checker.js +13 -0
  100. package/oclif.manifest.json +5038 -3635
  101. package/package.json +7 -1
@@ -3,10 +3,12 @@ export default class UnitTestList extends BaseCommand {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static flags: {
6
+ workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
8
+ per_page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
6
9
  branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
10
  'obj-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
11
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
- workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
12
  config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
13
  profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
14
  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 UnitTestList extends BaseCommand {
4
5
  static description = 'List all unit tests in a workspace';
5
6
  static examples = [
@@ -30,6 +31,7 @@ Unit tests in workspace 5:
30
31
  options: ['summary', 'json'],
31
32
  required: false,
32
33
  }),
34
+ ...pagingFlags('envelope', { maxPerPage: 10_000 }),
33
35
  workspace: Flags.string({
34
36
  char: 'w',
35
37
  description: 'Workspace ID (uses profile workspace if not provided)',
@@ -43,8 +45,7 @@ Unit tests in workspace 5:
43
45
  if (!workspaceId) {
44
46
  this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
45
47
  }
46
- const params = new URLSearchParams();
47
- params.set('per_page', '10000');
48
+ const params = new URLSearchParams(buildPagingParams(flags, 'envelope'));
48
49
  if (flags.branch) {
49
50
  params.set('branch', flags.branch);
50
51
  }
@@ -64,19 +65,10 @@ Unit tests in workspace 5:
64
65
  const errorText = await response.text();
65
66
  this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
66
67
  }
67
- const data = await response.json();
68
- let tests;
69
- if (Array.isArray(data)) {
70
- tests = data;
71
- }
72
- else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
73
- tests = data.items;
74
- }
75
- else {
76
- this.error('Unexpected API response format');
77
- }
68
+ const list = normalizeListResponse(await response.json());
69
+ const tests = list.items;
78
70
  if (flags.output === 'json') {
79
- this.log(JSON.stringify(tests, null, 2));
71
+ this.log(JSON.stringify(buildPagingJson(list, { page: flags.page, perPage: flags.per_page, tier: 'envelope' }), null, 2));
80
72
  }
81
73
  else {
82
74
  if (tests.length === 0) {
@@ -88,6 +80,9 @@ Unit tests in workspace 5:
88
80
  this.log(` - ${test.name} (ID: ${test.id}) [${test.obj_type}: ${test.obj_name}]`);
89
81
  }
90
82
  }
83
+ const footer = formatPagingFooter(list, { noun: 'unit test', page: flags.page, tier: 'envelope' });
84
+ if (footer)
85
+ this.log(footer);
91
86
  }
92
87
  }
93
88
  catch (error) {
@@ -4,6 +4,7 @@ export default class UnitTestRunAll 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
  workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -1,5 +1,7 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../base-command.js';
3
+ import { createOrderedEmitter, mapWithConcurrency } from '../../../utils/concurrency.js';
4
+ import { collectAllPages, INTERNAL_PAGE_SIZE, normalizeListResponse } from '../../../utils/paging.js';
3
5
  export default class UnitTestRunAll extends BaseCommand {
4
6
  static description = 'Run all unit tests in a workspace';
5
7
  static examples = [
@@ -22,6 +24,11 @@ Results: 2 passed, 1 failed
22
24
  description: 'Branch to run tests from (uses profile branch if not provided, then the live branch)',
23
25
  required: false,
24
26
  }),
27
+ concurrency: Flags.integer({
28
+ default: 1,
29
+ description: 'Run this many tests in parallel. Tests share the workspace database, so raise this only when your tests do not depend on shared state.',
30
+ required: false,
31
+ }),
25
32
  'obj-type': Flags.string({
26
33
  description: 'Filter by object type',
27
34
  options: ['function', 'query', 'middleware'],
@@ -53,36 +60,36 @@ Results: 2 passed, 1 failed
53
60
  const branch = flags.branch ?? profile.branch;
54
61
  try {
55
62
  // Step 1: List all unit tests
56
- const listParams = new URLSearchParams();
57
- listParams.set('per_page', '10000');
58
- if (branch) {
59
- listParams.set('branch', branch);
60
- }
61
- if (flags['obj-type']) {
62
- listParams.set('obj_type', flags['obj-type']);
63
- }
64
- const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
65
- headers: {
66
- accept: 'application/json',
67
- Authorization: `Bearer ${profile.access_token}`,
63
+ // Page the list rather than asking for everything at once: 100 per
64
+ // request is plenty for typical suites and does not make the instance
65
+ // build one enormous response. Termination is the server's own nextPage,
66
+ // never "the page came back full".
67
+ const branchValue = branch;
68
+ const tests = await collectAllPages(async (page) => {
69
+ const listParams = new URLSearchParams();
70
+ listParams.set('page', String(page));
71
+ listParams.set('per_page', String(INTERNAL_PAGE_SIZE));
72
+ if (branchValue)
73
+ listParams.set('branch', branchValue);
74
+ if (flags['obj-type'])
75
+ listParams.set('obj_type', flags['obj-type']);
76
+ const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
77
+ headers: {
78
+ accept: 'application/json',
79
+ Authorization: `Bearer ${profile.access_token}`,
80
+ },
81
+ method: 'GET',
82
+ }, flags.verbose, profile.access_token);
83
+ if (!listResponse.ok) {
84
+ const errorText = await listResponse.text();
85
+ this.error(`Failed to list unit tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
86
+ }
87
+ return normalizeListResponse(await listResponse.json());
88
+ }, {
89
+ onTruncate: (collected) => {
90
+ this.warn(`Stopped after ${collected} unit tests — the list did not terminate. Some tests may not have run.`);
68
91
  },
69
- method: 'GET',
70
- }, flags.verbose, profile.access_token);
71
- if (!listResponse.ok) {
72
- const errorText = await listResponse.text();
73
- this.error(`Failed to list unit tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
74
- }
75
- const data = (await listResponse.json());
76
- let tests;
77
- if (Array.isArray(data)) {
78
- tests = data;
79
- }
80
- else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
81
- tests = data.items;
82
- }
83
- else {
84
- this.error('Unexpected API response format');
85
- }
92
+ });
86
93
  if (tests.length === 0) {
87
94
  this.log('No unit tests found');
88
95
  return;
@@ -92,7 +99,15 @@ Results: 2 passed, 1 failed
92
99
  }
93
100
  // Step 2: Run each test
94
101
  const results = [];
95
- for (const test of tests) {
102
+ const runOne = async (test) => {
103
+ // Shadowed accumulators: the body below is the original sequential
104
+ // logic, but it records output instead of printing it so a concurrent
105
+ // run can still emit in input order.
106
+ const results = [];
107
+ const lines = [];
108
+ const log = (message) => {
109
+ lines.push(message);
110
+ };
96
111
  const runUrl = `${baseUrl}/${test.id}/run`;
97
112
  try {
98
113
  const runResponse = await this.verboseFetch(runUrl, {
@@ -115,10 +130,10 @@ Results: 2 passed, 1 failed
115
130
  };
116
131
  results.push(result);
117
132
  if (flags.output === 'summary') {
118
- this.log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
119
- this.log(` Error: API error ${runResponse.status}`);
133
+ log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
134
+ log(` Error: API error ${runResponse.status}`);
120
135
  }
121
- continue;
136
+ return { lines, results };
122
137
  }
123
138
  const runResult = (await runResponse.json());
124
139
  const passed = runResult.status === 'ok';
@@ -133,13 +148,13 @@ Results: 2 passed, 1 failed
133
148
  results.push(result);
134
149
  if (flags.output === 'summary') {
135
150
  if (passed) {
136
- this.log(`PASS ${test.name} [${test.obj_type}: ${test.obj_name}]`);
151
+ log(`PASS ${test.name} [${test.obj_type}: ${test.obj_name}]`);
137
152
  }
138
153
  else {
139
- this.log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
154
+ log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
140
155
  for (const expect of failedExpects) {
141
156
  if (expect.message) {
142
- this.log(` Error: ${expect.message}`);
157
+ log(` Error: ${expect.message}`);
143
158
  }
144
159
  }
145
160
  }
@@ -155,16 +170,29 @@ Results: 2 passed, 1 failed
155
170
  status: 'fail',
156
171
  });
157
172
  if (flags.output === 'summary') {
158
- this.log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
159
- this.log(` Error: ${message}`);
173
+ log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
174
+ log(` Error: ${message}`);
160
175
  }
161
176
  }
162
- }
177
+ return { lines, results };
178
+ };
179
+ // Print in input order as each test settles, so concurrent output stays
180
+ // diffable against a sequential run.
181
+ const emitter = createOrderedEmitter((settled) => {
182
+ for (const line of settled.lines)
183
+ this.log(line);
184
+ });
185
+ const perTest = await mapWithConcurrency(tests, flags.concurrency, async (test, index) => {
186
+ const settled = await runOne(test);
187
+ emitter.settle(index, settled);
188
+ return settled;
189
+ });
190
+ results.push(...perTest.flatMap((r) => r.results));
163
191
  // Step 3: Summary
164
192
  const passed = results.filter((r) => r.status === 'pass').length;
165
193
  const failed = results.filter((r) => r.status === 'fail').length;
166
194
  if (flags.output === 'json') {
167
- this.log(JSON.stringify({ passed, failed, results }, null, 2));
195
+ this.log(JSON.stringify({ failed, passed, results }, null, 2));
168
196
  }
169
197
  else {
170
198
  this.log(`\nResults: ${passed} passed, ${failed} failed`);
@@ -3,9 +3,11 @@ export default class WorkflowTestList extends BaseCommand {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static flags: {
6
+ workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
8
+ per_page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
6
9
  branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
10
  output: 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
11
  config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
12
  profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
13
  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 WorkflowTestList extends BaseCommand {
4
5
  static description = 'List all workflow tests in a workspace';
5
6
  static examples = [
@@ -25,6 +26,7 @@ Workflow tests in workspace 5:
25
26
  options: ['summary', 'json'],
26
27
  required: false,
27
28
  }),
29
+ ...pagingFlags('envelope', { maxPerPage: 10_000 }),
28
30
  workspace: Flags.string({
29
31
  char: 'w',
30
32
  description: 'Workspace ID (uses profile workspace if not provided)',
@@ -38,7 +40,8 @@ Workflow tests in workspace 5:
38
40
  if (!workspaceId) {
39
41
  this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
40
42
  }
41
- const params = new URLSearchParams({ include_xanoscript: 'false' });
43
+ const params = new URLSearchParams(buildPagingParams(flags, 'envelope'));
44
+ params.set('include_xanoscript', 'false');
42
45
  if (flags.branch) {
43
46
  params.set('branch', flags.branch);
44
47
  }
@@ -55,19 +58,10 @@ Workflow tests in workspace 5:
55
58
  const errorText = await response.text();
56
59
  this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
57
60
  }
58
- const data = await response.json();
59
- let tests;
60
- if (Array.isArray(data)) {
61
- tests = data;
62
- }
63
- else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
64
- tests = data.items;
65
- }
66
- else {
67
- this.error('Unexpected API response format');
68
- }
61
+ const list = normalizeListResponse(await response.json());
62
+ const tests = list.items;
69
63
  if (flags.output === 'json') {
70
- this.log(JSON.stringify(tests, null, 2));
64
+ this.log(JSON.stringify(buildPagingJson(list, { page: flags.page, perPage: flags.per_page, tier: 'envelope' }), null, 2));
71
65
  }
72
66
  else {
73
67
  if (tests.length === 0) {
@@ -80,6 +74,9 @@ Workflow tests in workspace 5:
80
74
  this.log(` - ${test.name} (ID: ${test.id})${desc}`);
81
75
  }
82
76
  }
77
+ const footer = formatPagingFooter(list, { noun: 'workflow test', page: flags.page, tier: 'envelope' });
78
+ if (footer)
79
+ this.log(footer);
83
80
  }
84
81
  }
85
82
  catch (error) {
@@ -4,6 +4,7 @@ export default class WorkflowTestRunAll 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
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
9
  workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
10
  config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -1,5 +1,7 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../base-command.js';
3
+ import { createOrderedEmitter, mapWithConcurrency } from '../../../utils/concurrency.js';
4
+ import { collectAllPages, INTERNAL_PAGE_SIZE, normalizeListResponse } from '../../../utils/paging.js';
3
5
  export default class WorkflowTestRunAll extends BaseCommand {
4
6
  static description = 'Run all workflow tests in a workspace';
5
7
  static examples = [
@@ -22,6 +24,11 @@ Results: 2 passed, 1 failed (2.691s total)
22
24
  description: 'Filter by branch name',
23
25
  required: false,
24
26
  }),
27
+ concurrency: Flags.integer({
28
+ default: 1,
29
+ description: 'Run this many tests in parallel. Tests share the workspace database, so raise this only when your tests do not depend on shared state.',
30
+ required: false,
31
+ }),
25
32
  output: Flags.string({
26
33
  char: 'o',
27
34
  default: 'summary',
@@ -45,32 +52,35 @@ Results: 2 passed, 1 failed (2.691s total)
45
52
  const baseUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/workflow_test`;
46
53
  try {
47
54
  // Step 1: List all workflow tests
48
- const listParams = new URLSearchParams({ include_xanoscript: 'false' });
49
- if (flags.branch) {
50
- listParams.set('branch', flags.branch);
51
- }
52
- const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
53
- headers: {
54
- accept: 'application/json',
55
- Authorization: `Bearer ${profile.access_token}`,
55
+ // Page the list rather than asking for everything at once: 100 per
56
+ // request is plenty for typical suites and does not make the instance
57
+ // build one enormous response. Termination is the server's own nextPage,
58
+ // never "the page came back full".
59
+ const branchValue = flags.branch;
60
+ const tests = await collectAllPages(async (page) => {
61
+ const listParams = new URLSearchParams();
62
+ listParams.set('page', String(page));
63
+ listParams.set('per_page', String(INTERNAL_PAGE_SIZE));
64
+ if (branchValue)
65
+ listParams.set('branch', branchValue);
66
+ listParams.set('include_xanoscript', 'false');
67
+ const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
68
+ headers: {
69
+ accept: 'application/json',
70
+ Authorization: `Bearer ${profile.access_token}`,
71
+ },
72
+ method: 'GET',
73
+ }, flags.verbose, profile.access_token);
74
+ if (!listResponse.ok) {
75
+ const errorText = await listResponse.text();
76
+ this.error(`Failed to list workflow tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
77
+ }
78
+ return normalizeListResponse(await listResponse.json());
79
+ }, {
80
+ onTruncate: (collected) => {
81
+ this.warn(`Stopped after ${collected} workflow tests — the list did not terminate. Some tests may not have run.`);
56
82
  },
57
- method: 'GET',
58
- }, flags.verbose, profile.access_token);
59
- if (!listResponse.ok) {
60
- const errorText = await listResponse.text();
61
- this.error(`Failed to list workflow tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
62
- }
63
- const data = (await listResponse.json());
64
- let tests;
65
- if (Array.isArray(data)) {
66
- tests = data;
67
- }
68
- else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
69
- tests = data.items;
70
- }
71
- else {
72
- this.error('Unexpected API response format');
73
- }
83
+ });
74
84
  if (tests.length === 0) {
75
85
  this.log('No workflow tests found');
76
86
  return;
@@ -81,7 +91,15 @@ Results: 2 passed, 1 failed (2.691s total)
81
91
  // Step 2: Run each test
82
92
  const results = [];
83
93
  let totalTiming = 0;
84
- for (const test of tests) {
94
+ const runOne = async (test) => {
95
+ // Shadowed accumulators: the body below is the original sequential
96
+ // logic, but it records output instead of printing it so a concurrent
97
+ // run can still emit in input order.
98
+ const results = [];
99
+ const lines = [];
100
+ const log = (message) => {
101
+ lines.push(message);
102
+ };
85
103
  const runUrl = `${baseUrl}/${test.id}/run`;
86
104
  try {
87
105
  const runResponse = await this.verboseFetch(runUrl, {
@@ -102,10 +120,10 @@ Results: 2 passed, 1 failed (2.691s total)
102
120
  };
103
121
  results.push(result);
104
122
  if (flags.output === 'summary') {
105
- this.log(`FAIL ${test.name} (0.000s)`);
106
- this.log(` Error: API error ${runResponse.status}`);
123
+ log(`FAIL ${test.name} (0.000s)`);
124
+ log(` Error: API error ${runResponse.status}`);
107
125
  }
108
- continue;
126
+ return { lines, results };
109
127
  }
110
128
  const runResult = (await runResponse.json());
111
129
  const passed = runResult.status === 'ok';
@@ -120,12 +138,12 @@ Results: 2 passed, 1 failed (2.691s total)
120
138
  if (flags.output === 'summary') {
121
139
  const timing = `(${runResult.timing.toFixed(3)}s)`;
122
140
  if (passed) {
123
- this.log(`PASS ${test.name} ${timing}`);
141
+ log(`PASS ${test.name} ${timing}`);
124
142
  }
125
143
  else {
126
- this.log(`FAIL ${test.name} ${timing}`);
144
+ log(`FAIL ${test.name} ${timing}`);
127
145
  if (runResult.message) {
128
- this.log(` Error: ${runResult.message}`);
146
+ log(` Error: ${runResult.message}`);
129
147
  }
130
148
  }
131
149
  }
@@ -139,16 +157,29 @@ Results: 2 passed, 1 failed (2.691s total)
139
157
  timing: 0,
140
158
  });
141
159
  if (flags.output === 'summary') {
142
- this.log(`FAIL ${test.name} (0.000s)`);
143
- this.log(` Error: ${message}`);
160
+ log(`FAIL ${test.name} (0.000s)`);
161
+ log(` Error: ${message}`);
144
162
  }
145
163
  }
146
- }
164
+ return { lines, results };
165
+ };
166
+ // Print in input order as each test settles, so concurrent output stays
167
+ // diffable against a sequential run.
168
+ const emitter = createOrderedEmitter((settled) => {
169
+ for (const line of settled.lines)
170
+ this.log(line);
171
+ });
172
+ const perTest = await mapWithConcurrency(tests, flags.concurrency, async (test, index) => {
173
+ const settled = await runOne(test);
174
+ emitter.settle(index, settled);
175
+ return settled;
176
+ });
177
+ results.push(...perTest.flatMap((r) => r.results));
147
178
  // Step 3: Summary
148
179
  const passed = results.filter((r) => r.status === 'pass').length;
149
180
  const failed = results.filter((r) => r.status === 'fail').length;
150
181
  if (flags.output === 'json') {
151
- this.log(JSON.stringify({ passed, failed, total_timing: totalTiming, results }, null, 2));
182
+ this.log(JSON.stringify({ failed, passed, results, total_timing: totalTiming }, null, 2));
152
183
  }
153
184
  else {
154
185
  this.log(`\nResults: ${passed} passed, ${failed} failed (${totalTiming.toFixed(3)}s total)`);
@@ -5,7 +5,7 @@ import * as os from 'node:os';
5
5
  import * as path from 'node:path';
6
6
  import snakeCase from 'lodash.snakecase';
7
7
  import BaseCommand, { buildUserAgent } from '../../../../base-command.js';
8
- import { buildApiGroupFolderResolver, parseDocument } from '../../../../utils/document-parser.js';
8
+ import { buildApiGroupFolderResolver, buildChannelServerResolver, channelPathSegments, parseDocument, } from '../../../../utils/document-parser.js';
9
9
  export default class GitPull extends BaseCommand {
10
10
  static description = 'Pull XanoScript files from a git repository into a local directory';
11
11
  static examples = [
@@ -97,10 +97,13 @@ export default class GitPull extends BaseCommand {
97
97
  // Write documents to output directory using the same file tree logic as workspace pull
98
98
  fs.mkdirSync(outputDir, { recursive: true });
99
99
  const getApiGroupFolder = buildApiGroupFolderResolver(documents, snakeCase);
100
+ // Resolve a realtime v2 channel path -> owning realtime_server name, so
101
+ // messages can nest under their channel's server (see resolver docs).
102
+ const getChannelServer = buildChannelServerResolver(documents);
100
103
  const filenameCounters = new Map();
101
104
  let writtenCount = 0;
102
105
  for (const doc of documents) {
103
- const { baseName, typeDir } = this.resolveOutputPath(outputDir, doc, getApiGroupFolder);
106
+ const { baseName, typeDir } = this.resolveOutputPath(outputDir, doc, getApiGroupFolder, getChannelServer);
104
107
  fs.mkdirSync(typeDir, { recursive: true });
105
108
  // Track duplicates per directory
106
109
  const dirKey = path.relative(outputDir, typeDir);
@@ -325,7 +328,7 @@ export default class GitPull extends BaseCommand {
325
328
  * Resolve the output directory and base filename for a parsed document.
326
329
  * Uses the same type-to-directory mapping as workspace pull.
327
330
  */
328
- resolveOutputPath(outputDir, doc, getApiGroupFolder) {
331
+ resolveOutputPath(outputDir, doc, getApiGroupFolder, getChannelServer) {
329
332
  let typeDir;
330
333
  let baseName;
331
334
  if (doc.type === 'workspace') {
@@ -373,6 +376,53 @@ export default class GitPull extends BaseCommand {
373
376
  typeDir = path.join(outputDir, 'realtime', 'trigger');
374
377
  baseName = this.sanitizeFilename(doc.name);
375
378
  }
379
+ else if (doc.type === 'realtime_server') {
380
+ // Realtime v2 — see workspace/pull for the full rationale. Its own
381
+ // document is named after itself, mirroring api_group.
382
+ // realtime_server "chat" → realtime/server/chat/chat.xs
383
+ typeDir = path.join(outputDir, 'realtime', 'server', this.sanitizeFilename(doc.name));
384
+ baseName = this.sanitizeFilename(doc.name);
385
+ }
386
+ else if (doc.type === 'channel') {
387
+ // Realtime v2. The channel owns a directory named after itself (the full
388
+ // channel name, snake_cased into a single flat segment — like every other
389
+ // object), and its messages live in a message/ subfolder inside it. It
390
+ // nests under its owning realtime_server (from `server = "..."`):
391
+ // channel "rooms/{room_id}" (server "chat")
392
+ // → realtime/server/chat/channel/rooms_room_id/rooms_room_id.xs
393
+ //
394
+ // The channel name is snake_cased as a whole (rooms/{room_id} →
395
+ // rooms_room_id), so there is no path nesting and no bracket escaping —
396
+ // the on-disk name is a display form; the .xs content keeps the real
397
+ // name. A channel with no resolvable server (e.g. a pre-server export)
398
+ // falls back to the legacy flat channel/<path>/_channel.xs layout.
399
+ if (doc.server) {
400
+ typeDir = path.join(outputDir, 'realtime', 'server', this.sanitizeFilename(doc.server), 'channel', snakeCase(doc.name));
401
+ baseName = snakeCase(doc.name);
402
+ }
403
+ else {
404
+ typeDir = path.join(outputDir, 'channel', ...channelPathSegments(doc.name, snakeCase));
405
+ baseName = '_channel';
406
+ }
407
+ }
408
+ else if (doc.type === 'message' && doc.channel) {
409
+ // Realtime v2 message → nests in a message/ subfolder under its channel,
410
+ // under that channel's server. The message names only its channel; the
411
+ // server is resolved by looking that channel up in the same multidoc
412
+ // (two-pass resolve).
413
+ // message "post" on channel "rooms/{room_id}" (server "chat")
414
+ // → realtime/server/chat/channel/rooms_room_id/message/post.xs
415
+ //
416
+ // Nesting matters beyond tidiness: message names are unique only WITHIN
417
+ // a channel, so a flat message/ directory would collide when two
418
+ // channels both define e.g. "say". A channel whose server can't be
419
+ // resolved falls back to the legacy flat channel/ layout.
420
+ const messageServer = getChannelServer(doc.channel);
421
+ typeDir = messageServer
422
+ ? path.join(outputDir, 'realtime', 'server', this.sanitizeFilename(messageServer), 'channel', snakeCase(doc.channel), 'message')
423
+ : path.join(outputDir, 'channel', ...channelPathSegments(doc.channel, snakeCase));
424
+ baseName = this.sanitizeFilename(doc.name);
425
+ }
376
426
  else if (doc.type === 'api_group') {
377
427
  const groupFolder = getApiGroupFolder(doc.name);
378
428
  typeDir = path.join(outputDir, 'api', groupFolder);
@@ -1,5 +1,6 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../base-command.js';
3
+ import { buildPagingJson, formatPagingFooter, normalizeListResponse } from '../../../utils/paging.js';
3
4
  export default class WorkspaceList extends BaseCommand {
4
5
  static description = 'List all workspaces from the Xano Metadata API';
5
6
  static examples = [
@@ -71,24 +72,14 @@ Available workspaces:
71
72
  const errorText = await response.text();
72
73
  this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
73
74
  }
74
- const data = (await response.json());
75
- // Handle different response formats
76
- let workspaces;
77
- if (Array.isArray(data)) {
78
- workspaces = data;
79
- }
80
- else if (data && typeof data === 'object' && 'workspaces' in data && Array.isArray(data.workspaces)) {
81
- workspaces = data.workspaces;
82
- }
83
- else {
84
- this.error('Unexpected API response format');
85
- }
75
+ const list = normalizeListResponse(await response.json(), ['workspaces']);
76
+ const workspaces = list.items;
86
77
  if (flags.latest) {
87
78
  workspaces.sort((a, b) => b.id - a.id);
88
79
  }
89
80
  // Output results
90
81
  if (flags.output === 'json') {
91
- this.log(JSON.stringify(workspaces, null, 2));
82
+ this.log(JSON.stringify(buildPagingJson(list, { tier: 'none' }), null, 2));
92
83
  }
93
84
  else {
94
85
  // summary format
@@ -107,6 +98,9 @@ Available workspaces:
107
98
  }
108
99
  }
109
100
  }
101
+ const footer = formatPagingFooter({ items: workspaces }, { noun: 'workspace', tier: 'none' });
102
+ if (footer)
103
+ this.log(footer);
110
104
  }
111
105
  }
112
106
  catch (error) {
@@ -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
  }