@xano/cli 1.0.6-beta.5 → 1.0.6-beta.7

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 (55) 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/static_host/build/list/index.d.ts +2 -3
  5. package/dist/commands/ephemeral/static_host/build/list/index.js +19 -40
  6. package/dist/commands/ephemeral/static_host/list/index.d.ts +2 -3
  7. package/dist/commands/ephemeral/static_host/list/index.js +20 -40
  8. package/dist/commands/function/list/index.d.ts +4 -4
  9. package/dist/commands/function/list/index.js +30 -38
  10. package/dist/commands/knowledge/list/index.js +2 -1
  11. package/dist/commands/platform/list/index.js +13 -14
  12. package/dist/commands/release/list/index.js +13 -14
  13. package/dist/commands/sandbox/env/list/index.js +4 -0
  14. package/dist/commands/sandbox/unit_test/list/index.d.ts +2 -0
  15. package/dist/commands/sandbox/unit_test/list/index.js +9 -14
  16. package/dist/commands/sandbox/unit_test/run_all/index.d.ts +1 -0
  17. package/dist/commands/sandbox/unit_test/run_all/index.js +67 -37
  18. package/dist/commands/sandbox/workflow_test/list/index.d.ts +2 -0
  19. package/dist/commands/sandbox/workflow_test/list/index.js +9 -14
  20. package/dist/commands/sandbox/workflow_test/run_all/index.d.ts +1 -0
  21. package/dist/commands/sandbox/workflow_test/run_all/index.js +65 -35
  22. package/dist/commands/static_host/build/list/index.d.ts +2 -3
  23. package/dist/commands/static_host/build/list/index.js +19 -40
  24. package/dist/commands/static_host/build/pull/index.js +12 -9
  25. package/dist/commands/static_host/list/index.d.ts +2 -3
  26. package/dist/commands/static_host/list/index.js +20 -40
  27. package/dist/commands/static_host/migrate/index.js +14 -10
  28. package/dist/commands/tenant/backup/list/index.js +13 -14
  29. package/dist/commands/tenant/cluster/list/index.js +9 -10
  30. package/dist/commands/tenant/env/list/index.js +4 -0
  31. package/dist/commands/tenant/list/index.js +19 -20
  32. package/dist/commands/tenant/snapshot/list/index.js +5 -1
  33. package/dist/commands/tenant/unit_test/list/index.d.ts +4 -2
  34. package/dist/commands/tenant/unit_test/list/index.js +9 -14
  35. package/dist/commands/tenant/unit_test/run_all/index.d.ts +1 -0
  36. package/dist/commands/tenant/unit_test/run_all/index.js +67 -37
  37. package/dist/commands/tenant/workflow_test/list/index.d.ts +4 -2
  38. package/dist/commands/tenant/workflow_test/list/index.js +9 -14
  39. package/dist/commands/tenant/workflow_test/run_all/index.d.ts +1 -0
  40. package/dist/commands/tenant/workflow_test/run_all/index.js +65 -35
  41. package/dist/commands/unit_test/list/index.d.ts +3 -1
  42. package/dist/commands/unit_test/list/index.js +9 -14
  43. package/dist/commands/unit_test/run_all/index.d.ts +1 -0
  44. package/dist/commands/unit_test/run_all/index.js +68 -40
  45. package/dist/commands/workflow_test/list/index.d.ts +3 -1
  46. package/dist/commands/workflow_test/list/index.js +10 -13
  47. package/dist/commands/workflow_test/run_all/index.d.ts +1 -0
  48. package/dist/commands/workflow_test/run_all/index.js +67 -36
  49. package/dist/commands/workspace/list/index.js +7 -13
  50. package/dist/utils/concurrency.d.ts +29 -0
  51. package/dist/utils/concurrency.js +54 -0
  52. package/dist/utils/paging.d.ts +145 -0
  53. package/dist/utils/paging.js +199 -0
  54. package/oclif.manifest.json +3009 -2883
  55. 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 } 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
  }
@@ -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) {
@@ -3,6 +3,8 @@ export default class SandboxUnitTestList extends BaseCommand {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static flags: {
6
+ page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
7
+ per_page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
6
8
  branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
9
  'obj-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
10
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
@@ -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 SandboxUnitTestList extends BaseCommand {
4
5
  static description = 'List all unit tests for a sandbox environment';
5
6
  static examples = [
@@ -28,12 +29,12 @@ Unit tests:
28
29
  options: ['summary', 'json'],
29
30
  required: false,
30
31
  }),
32
+ ...pagingFlags('envelope', { maxPerPage: 10_000 }),
31
33
  };
32
34
  async run() {
33
35
  const { flags } = await this.parse(SandboxUnitTestList);
34
36
  const { profile } = this.resolveProfile(flags);
35
- const params = new URLSearchParams();
36
- params.set('per_page', '10000');
37
+ const params = new URLSearchParams(buildPagingParams(flags, 'envelope'));
37
38
  if (flags.branch)
38
39
  params.set('branch', flags.branch);
39
40
  if (flags['obj-type'])
@@ -51,19 +52,10 @@ Unit tests:
51
52
  const message = await this.parseApiError(response, 'API request failed');
52
53
  this.error(message);
53
54
  }
54
- const data = (await response.json());
55
- let tests;
56
- if (Array.isArray(data)) {
57
- tests = data;
58
- }
59
- else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
60
- tests = data.items;
61
- }
62
- else {
63
- this.error('Unexpected API response format');
64
- }
55
+ const list = normalizeListResponse(await response.json());
56
+ const tests = list.items;
65
57
  if (flags.output === 'json') {
66
- this.log(JSON.stringify(tests, null, 2));
58
+ this.log(JSON.stringify(buildPagingJson(list, { page: flags.page, perPage: flags.per_page, tier: 'envelope' }), null, 2));
67
59
  }
68
60
  else {
69
61
  if (tests.length === 0) {
@@ -75,6 +67,9 @@ Unit tests:
75
67
  this.log(` - ${test.name} (ID: ${test.id}) [${test.obj_type}: ${test.obj_name}]`);
76
68
  }
77
69
  }
70
+ const footer = formatPagingFooter(list, { noun: 'unit test', page: flags.page, tier: 'envelope' });
71
+ if (footer)
72
+ this.log(footer);
78
73
  }
79
74
  }
80
75
  catch (error) {
@@ -4,6 +4,7 @@ export default class SandboxUnitTestRunAll 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
  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 SandboxUnitTestRunAll extends BaseCommand {
4
6
  static description = 'Run all unit tests for a sandbox environment';
5
7
  static examples = [
@@ -21,6 +23,11 @@ Results: 4 passed, 1 failed
21
23
  description: 'Filter by branch name',
22
24
  required: false,
23
25
  }),
26
+ concurrency: Flags.integer({
27
+ default: 1,
28
+ 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.',
29
+ required: false,
30
+ }),
24
31
  'obj-type': Flags.string({
25
32
  description: 'Filter by object type',
26
33
  options: ['function', 'query', 'middleware'],
@@ -40,34 +47,36 @@ Results: 4 passed, 1 failed
40
47
  const baseUrl = `${profile.instance_origin}/api:meta/sandbox/unit_test`;
41
48
  try {
42
49
  // Step 1: List all unit tests
43
- const listParams = new URLSearchParams();
44
- listParams.set('per_page', '10000');
45
- if (flags.branch)
46
- listParams.set('branch', flags.branch);
47
- if (flags['obj-type'])
48
- listParams.set('obj_type', flags['obj-type']);
49
- const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
50
- headers: {
51
- accept: 'application/json',
52
- Authorization: `Bearer ${profile.access_token}`,
50
+ // Page the list rather than asking for everything at once: 100 per
51
+ // request is plenty for typical suites and does not make the instance
52
+ // build one enormous response. Termination is the server's own nextPage,
53
+ // never "the page came back full".
54
+ const branchValue = flags.branch;
55
+ const tests = await collectAllPages(async (page) => {
56
+ const listParams = new URLSearchParams();
57
+ listParams.set('page', String(page));
58
+ listParams.set('per_page', String(INTERNAL_PAGE_SIZE));
59
+ if (branchValue)
60
+ listParams.set('branch', branchValue);
61
+ if (flags['obj-type'])
62
+ listParams.set('obj_type', flags['obj-type']);
63
+ const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
64
+ headers: {
65
+ accept: 'application/json',
66
+ Authorization: `Bearer ${profile.access_token}`,
67
+ },
68
+ method: 'GET',
69
+ }, flags.verbose, profile.access_token);
70
+ if (!listResponse.ok) {
71
+ const errorText = await listResponse.text();
72
+ this.error(`Failed to list unit tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
73
+ }
74
+ return normalizeListResponse(await listResponse.json());
75
+ }, {
76
+ onTruncate: (collected) => {
77
+ this.warn(`Stopped after ${collected} unit tests — the list did not terminate. Some tests may not have run.`);
53
78
  },
54
- method: 'GET',
55
- }, flags.verbose, profile.access_token);
56
- if (!listResponse.ok) {
57
- const errorText = await listResponse.text();
58
- this.error(`Failed to list unit tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
59
- }
60
- const data = (await listResponse.json());
61
- let tests;
62
- if (Array.isArray(data)) {
63
- tests = data;
64
- }
65
- else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
66
- tests = data.items;
67
- }
68
- else {
69
- this.error('Unexpected API response format');
70
- }
79
+ });
71
80
  if (tests.length === 0) {
72
81
  this.log('No unit tests found');
73
82
  return;
@@ -77,7 +86,15 @@ Results: 4 passed, 1 failed
77
86
  }
78
87
  // Step 2: Run each test
79
88
  const results = [];
80
- for (const test of tests) {
89
+ const runOne = async (test) => {
90
+ // Shadowed accumulators: the body below is the original sequential
91
+ // logic, but it records output instead of printing it so a concurrent
92
+ // run can still emit in input order.
93
+ const results = [];
94
+ const lines = [];
95
+ const log = (message) => {
96
+ lines.push(message);
97
+ };
81
98
  const runUrl = `${baseUrl}/${test.id}/run`;
82
99
  try {
83
100
  const runResponse = await this.verboseFetch(runUrl, {
@@ -98,10 +115,10 @@ Results: 4 passed, 1 failed
98
115
  status: 'fail',
99
116
  });
100
117
  if (flags.output === 'summary') {
101
- this.log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
102
- this.log(` Error: API error ${runResponse.status}`);
118
+ log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
119
+ log(` Error: API error ${runResponse.status}`);
103
120
  }
104
- continue;
121
+ return { lines, results };
105
122
  }
106
123
  const runResult = (await runResponse.json());
107
124
  const passed = runResult.status === 'ok';
@@ -115,13 +132,13 @@ Results: 4 passed, 1 failed
115
132
  });
116
133
  if (flags.output === 'summary') {
117
134
  if (passed) {
118
- this.log(`PASS ${test.name} [${test.obj_type}: ${test.obj_name}]`);
135
+ log(`PASS ${test.name} [${test.obj_type}: ${test.obj_name}]`);
119
136
  }
120
137
  else {
121
- this.log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
138
+ log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
122
139
  for (const expect of failedExpects) {
123
140
  if (expect.message) {
124
- this.log(` Error: ${expect.message}`);
141
+ log(` Error: ${expect.message}`);
125
142
  }
126
143
  }
127
144
  }
@@ -139,11 +156,24 @@ Results: 4 passed, 1 failed
139
156
  status: 'fail',
140
157
  });
141
158
  if (flags.output === 'summary') {
142
- this.log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
143
- this.log(` Error: ${message}`);
159
+ log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
160
+ log(` Error: ${message}`);
144
161
  }
145
162
  }
146
- }
163
+ return { lines, results };
164
+ };
165
+ // Print in input order as each test settles, so concurrent output stays
166
+ // diffable against a sequential run.
167
+ const emitter = createOrderedEmitter((settled) => {
168
+ for (const line of settled.lines)
169
+ this.log(line);
170
+ });
171
+ const perTest = await mapWithConcurrency(tests, flags.concurrency, async (test, index) => {
172
+ const settled = await runOne(test);
173
+ emitter.settle(index, settled);
174
+ return settled;
175
+ });
176
+ results.push(...perTest.flatMap((r) => r.results));
147
177
  // Step 3: Summary
148
178
  const passed = results.filter((r) => r.status === 'pass').length;
149
179
  const failed = results.filter((r) => r.status === 'fail').length;
@@ -3,6 +3,8 @@ export default class SandboxWorkflowTestList extends BaseCommand {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static flags: {
6
+ page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
7
+ per_page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
6
8
  branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
9
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
10
  config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -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 SandboxWorkflowTestList extends BaseCommand {
4
5
  static description = 'List workflow tests for a sandbox environment';
5
6
  static examples = [
@@ -23,12 +24,12 @@ Workflow tests:
23
24
  options: ['summary', 'json'],
24
25
  required: false,
25
26
  }),
27
+ ...pagingFlags('envelope', { maxPerPage: 10_000 }),
26
28
  };
27
29
  async run() {
28
30
  const { flags } = await this.parse(SandboxWorkflowTestList);
29
31
  const { profile } = this.resolveProfile(flags);
30
- const params = new URLSearchParams();
31
- params.set('per_page', '10000');
32
+ const params = new URLSearchParams(buildPagingParams(flags, 'envelope'));
32
33
  if (flags.branch)
33
34
  params.set('branch', flags.branch);
34
35
  const apiUrl = `${profile.instance_origin}/api:meta/sandbox/workflow_test?${params}`;
@@ -44,19 +45,10 @@ Workflow tests:
44
45
  const message = await this.parseApiError(response, 'API request failed');
45
46
  this.error(message);
46
47
  }
47
- const data = (await response.json());
48
- let tests;
49
- if (Array.isArray(data)) {
50
- tests = data;
51
- }
52
- else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
53
- tests = data.items;
54
- }
55
- else {
56
- this.error('Unexpected API response format');
57
- }
48
+ const list = normalizeListResponse(await response.json());
49
+ const tests = list.items;
58
50
  if (flags.output === 'json') {
59
- this.log(JSON.stringify(tests, null, 2));
51
+ this.log(JSON.stringify(buildPagingJson(list, { page: flags.page, perPage: flags.per_page, tier: 'envelope' }), null, 2));
60
52
  }
61
53
  else {
62
54
  if (tests.length === 0) {
@@ -68,6 +60,9 @@ Workflow tests:
68
60
  this.log(` - ${test.name} (ID: ${test.id})`);
69
61
  }
70
62
  }
63
+ const footer = formatPagingFooter(list, { noun: 'workflow test', page: flags.page, tier: 'envelope' });
64
+ if (footer)
65
+ this.log(footer);
71
66
  }
72
67
  }
73
68
  catch (error) {
@@ -4,6 +4,7 @@ export default class SandboxWorkflowTestRunAll 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
  config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
10
  profile: 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 SandboxWorkflowTestRunAll extends BaseCommand {
4
6
  static description = 'Run all workflow tests for a sandbox environment';
5
7
  static examples = [
@@ -21,6 +23,11 @@ Results: 2 passed, 1 failed
21
23
  description: 'Filter by branch name',
22
24
  required: false,
23
25
  }),
26
+ concurrency: Flags.integer({
27
+ default: 1,
28
+ 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.',
29
+ required: false,
30
+ }),
24
31
  output: Flags.string({
25
32
  char: 'o',
26
33
  default: 'summary',
@@ -35,32 +42,34 @@ Results: 2 passed, 1 failed
35
42
  const baseUrl = `${profile.instance_origin}/api:meta/sandbox/workflow_test`;
36
43
  try {
37
44
  // Step 1: List all workflow tests
38
- const listParams = new URLSearchParams();
39
- listParams.set('per_page', '10000');
40
- if (flags.branch)
41
- listParams.set('branch', flags.branch);
42
- const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
43
- headers: {
44
- accept: 'application/json',
45
- Authorization: `Bearer ${profile.access_token}`,
45
+ // Page the list rather than asking for everything at once: 100 per
46
+ // request is plenty for typical suites and does not make the instance
47
+ // build one enormous response. Termination is the server's own nextPage,
48
+ // never "the page came back full".
49
+ const branchValue = flags.branch;
50
+ const tests = await collectAllPages(async (page) => {
51
+ const listParams = new URLSearchParams();
52
+ listParams.set('page', String(page));
53
+ listParams.set('per_page', String(INTERNAL_PAGE_SIZE));
54
+ if (branchValue)
55
+ listParams.set('branch', branchValue);
56
+ const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
57
+ headers: {
58
+ accept: 'application/json',
59
+ Authorization: `Bearer ${profile.access_token}`,
60
+ },
61
+ method: 'GET',
62
+ }, flags.verbose, profile.access_token);
63
+ if (!listResponse.ok) {
64
+ const errorText = await listResponse.text();
65
+ this.error(`Failed to list workflow tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
66
+ }
67
+ return normalizeListResponse(await listResponse.json());
68
+ }, {
69
+ onTruncate: (collected) => {
70
+ this.warn(`Stopped after ${collected} workflow tests — the list did not terminate. Some tests may not have run.`);
46
71
  },
47
- method: 'GET',
48
- }, flags.verbose, profile.access_token);
49
- if (!listResponse.ok) {
50
- const errorText = await listResponse.text();
51
- this.error(`Failed to list workflow tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
52
- }
53
- const data = (await listResponse.json());
54
- let tests;
55
- if (Array.isArray(data)) {
56
- tests = data;
57
- }
58
- else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
59
- tests = data.items;
60
- }
61
- else {
62
- this.error('Unexpected API response format');
63
- }
72
+ });
64
73
  if (tests.length === 0) {
65
74
  this.log('No workflow tests found');
66
75
  return;
@@ -70,7 +79,15 @@ Results: 2 passed, 1 failed
70
79
  }
71
80
  // Step 2: Run each test
72
81
  const results = [];
73
- for (const test of tests) {
82
+ const runOne = async (test) => {
83
+ // Shadowed accumulators: the body below is the original sequential
84
+ // logic, but it records output instead of printing it so a concurrent
85
+ // run can still emit in input order.
86
+ const results = [];
87
+ const lines = [];
88
+ const log = (message) => {
89
+ lines.push(message);
90
+ };
74
91
  const runUrl = `${baseUrl}/${test.id}/run`;
75
92
  try {
76
93
  const runResponse = await this.verboseFetch(runUrl, {
@@ -89,10 +106,10 @@ Results: 2 passed, 1 failed
89
106
  status: 'fail',
90
107
  });
91
108
  if (flags.output === 'summary') {
92
- this.log(`FAIL ${test.name}`);
93
- this.log(` Error: API error ${runResponse.status}`);
109
+ log(`FAIL ${test.name}`);
110
+ log(` Error: API error ${runResponse.status}`);
94
111
  }
95
- continue;
112
+ return { lines, results };
96
113
  }
97
114
  const runResult = (await runResponse.json());
98
115
  const passed = runResult.status === 'ok';
@@ -105,12 +122,12 @@ Results: 2 passed, 1 failed
105
122
  if (flags.output === 'summary') {
106
123
  const timing = runResult.timing ? ` (${runResult.timing}s)` : '';
107
124
  if (passed) {
108
- this.log(`PASS ${test.name}${timing}`);
125
+ log(`PASS ${test.name}${timing}`);
109
126
  }
110
127
  else {
111
- this.log(`FAIL ${test.name}${timing}`);
128
+ log(`FAIL ${test.name}${timing}`);
112
129
  if (runResult.message) {
113
- this.log(` Error: ${runResult.message}`);
130
+ log(` Error: ${runResult.message}`);
114
131
  }
115
132
  }
116
133
  }
@@ -125,11 +142,24 @@ Results: 2 passed, 1 failed
125
142
  status: 'fail',
126
143
  });
127
144
  if (flags.output === 'summary') {
128
- this.log(`FAIL ${test.name}`);
129
- this.log(` Error: ${message}`);
145
+ log(`FAIL ${test.name}`);
146
+ log(` Error: ${message}`);
130
147
  }
131
148
  }
132
- }
149
+ return { lines, results };
150
+ };
151
+ // Print in input order as each test settles, so concurrent output stays
152
+ // diffable against a sequential run.
153
+ const emitter = createOrderedEmitter((settled) => {
154
+ for (const line of settled.lines)
155
+ this.log(line);
156
+ });
157
+ const perTest = await mapWithConcurrency(tests, flags.concurrency, async (test, index) => {
158
+ const settled = await runOne(test);
159
+ emitter.settle(index, settled);
160
+ return settled;
161
+ });
162
+ results.push(...perTest.flatMap((r) => r.results));
133
163
  // Step 3: Summary
134
164
  const passed = results.filter((r) => r.status === 'pass').length;
135
165
  const failed = results.filter((r) => r.status === 'fail').length;
@@ -6,10 +6,9 @@ export default class StaticHostBuildList extends BaseCommand {
6
6
  static description: string;
7
7
  static examples: string[];
8
8
  static flags: {
9
- output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
- page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
11
- per_page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
12
9
  workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
11
+ output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
12
  config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
13
  profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
14
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
@@ -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 StaticHostBuildList extends BaseCommand {
4
5
  static args = {
5
6
  static_host: Args.string({
@@ -20,13 +21,16 @@ Available builds:
20
21
  - staging (ID: 2) - Status: completed
21
22
  `,
22
23
  `$ xano static_host:build:list default -w 40 --output json
23
- [
24
- {
25
- "id": 1,
26
- "name": "v1.0.0",
27
- "status": "completed"
28
- }
29
- ]
24
+ {
25
+ "count": 1,
26
+ "page": 1,
27
+ "total": 12,
28
+ "items": [
29
+ {
30
+ "id": 52
31
+ }
32
+ ]
33
+ }
30
34
  `,
31
35
  `$ xano static_host:build:list default -p staging -o json --page 2
32
36
  [
@@ -46,16 +50,7 @@ Available builds:
46
50
  options: ['summary', 'json'],
47
51
  required: false,
48
52
  }),
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
- }),
53
+ ...pagingFlags('page-only-envelope', { fixedPerPage: 100 }),
59
54
  workspace: Flags.string({
60
55
  char: 'w',
61
56
  description: 'Workspace ID (optional if set in profile)',
@@ -79,13 +74,7 @@ Available builds:
79
74
  ` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
80
75
  }
81
76
  // Build query parameters
82
- const queryParams = new URLSearchParams({
83
- page: flags.page.toString(),
84
- });
85
- // Only add per_page if it's not the default value
86
- if (flags.per_page !== 50) {
87
- queryParams.append('per_page', flags.per_page.toString());
88
- }
77
+ const queryParams = new URLSearchParams(buildPagingParams(flags, 'page-only-envelope'));
89
78
  // Construct the API URL
90
79
  const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${args.static_host}/build?${queryParams.toString()}`;
91
80
  // Fetch builds from the API
@@ -101,24 +90,11 @@ Available builds:
101
90
  const errorText = await response.text();
102
91
  this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
103
92
  }
104
- const data = await response.json();
105
- // Handle different response formats
106
- let builds;
107
- if (Array.isArray(data)) {
108
- builds = data;
109
- }
110
- else if (data && typeof data === 'object' && 'builds' in data && Array.isArray(data.builds)) {
111
- builds = data.builds;
112
- }
113
- else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
114
- builds = data.items;
115
- }
116
- else {
117
- this.error('Unexpected API response format');
118
- }
93
+ const list = normalizeListResponse(await response.json(), ['builds']);
94
+ const builds = list.items;
119
95
  // Output results
120
96
  if (flags.output === 'json') {
121
- this.log(JSON.stringify(builds, null, 2));
97
+ this.log(JSON.stringify(buildPagingJson(list, { page: flags.page, tier: 'page-only-envelope' }), null, 2));
122
98
  }
123
99
  else {
124
100
  // summary format
@@ -137,6 +113,9 @@ Available builds:
137
113
  }
138
114
  }
139
115
  }
116
+ const footer = formatPagingFooter(list, { noun: 'build', page: flags.page, tier: 'page-only-envelope' });
117
+ if (footer)
118
+ this.log(footer);
140
119
  }
141
120
  }
142
121
  catch (error) {