@xano/cli 1.0.6-beta.5 → 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 (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 +141 -0
  53. package/dist/utils/paging.js +194 -0
  54. package/oclif.manifest.json +3017 -2891
  55. package/package.json +1 -1
@@ -2,6 +2,7 @@ import { Args, Flags } from '@oclif/core';
2
2
  import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
4
  import BaseCommand from '../../../../base-command.js';
5
+ import { normalizeListResponse } from '../../../../utils/paging.js';
5
6
  /**
6
7
  * Find the deployed `canonical` for a static host's env from a list of hosts.
7
8
  * Returns null when the host isn't present or nothing is deployed to that env.
@@ -240,9 +241,13 @@ Pulled the compiled/served output instead of the original source
240
241
  this.error(`No build is deployed to the '${env}' environment of static host '${staticHost}'`);
241
242
  }
242
243
  // 2. Page through builds to find the one matching the deployed canonical.
243
- const perPage = 100;
244
+ // per_page is not an input on this endpoint (the server fixes the page
245
+ // size), so it is not sent. Termination uses the response's own itemsTotal
246
+ // rather than "the page came back full", which would stop early and report
247
+ // a spurious not-found if the server page size ever differed.
248
+ let seen = 0;
244
249
  for (let page = 1;; page++) {
245
- const listUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${staticHost}/build?page=${page}&per_page=${perPage}`;
250
+ const listUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${staticHost}/build?page=${page}`;
246
251
  // eslint-disable-next-line no-await-in-loop
247
252
  const response = await this.verboseFetch(listUrl, {
248
253
  headers: {
@@ -257,17 +262,15 @@ Pulled the compiled/served output instead of the original source
257
262
  this.error(`Failed to list builds: ${response.status} ${response.statusText}\n${errorText}`);
258
263
  }
259
264
  // eslint-disable-next-line no-await-in-loop
260
- const data = (await response.json());
261
- const builds = Array.isArray(data)
262
- ? data
263
- : data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)
264
- ? data.items
265
- : [];
265
+ const list = normalizeListResponse(await response.json(), ['builds']);
266
+ const builds = list.items;
266
267
  const match = findBuildByCanonical(builds, canonical);
267
268
  if (match) {
268
269
  return String(match.id);
269
270
  }
270
- if (builds.length < perPage) {
271
+ seen += builds.length;
272
+ const exhausted = builds.length === 0 || (list.itemsTotal !== undefined && seen >= list.itemsTotal);
273
+ if (exhausted) {
271
274
  // Reached the last page without a match.
272
275
  this.error(`Could not find the build deployed to '${env}' (canonical ${canonical}) in the build list for '${staticHost}'`);
273
276
  }
@@ -4,10 +4,9 @@ export default class StaticHostList extends BaseCommand {
4
4
  static description: string;
5
5
  static examples: string[];
6
6
  static flags: {
7
- output: import("@oclif/core/interfaces").OptionFlag<string, 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>;
10
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
+ output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
10
  config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
11
  profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
12
  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 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
  }
@@ -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>;
@@ -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 TenantUnitTestRunAll extends BaseCommand {
4
6
  static description = 'Run all unit tests for a tenant';
5
7
  static examples = [
@@ -21,6 +23,11 @@ Results: 4 passed, 1 failed
21
23
  description: 'Branch to run tests from (uses profile branch if not provided, then the live branch)',
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'],
@@ -57,34 +64,36 @@ Results: 4 passed, 1 failed
57
64
  // from the same branch.
58
65
  const branch = flags.branch ?? profile.branch;
59
66
  try {
60
- const listParams = new URLSearchParams();
61
- listParams.set('per_page', '10000');
62
- if (branch)
63
- listParams.set('branch', branch);
64
- if (flags['obj-type'])
65
- listParams.set('obj_type', flags['obj-type']);
66
- const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
67
- headers: {
68
- accept: 'application/json',
69
- Authorization: `Bearer ${profile.access_token}`,
67
+ // Page the list rather than asking for everything at once: 100 per
68
+ // request is plenty for typical suites and does not make the instance
69
+ // build one enormous response. Termination is the server's own nextPage,
70
+ // never "the page came back full".
71
+ const branchValue = branch;
72
+ const tests = await collectAllPages(async (page) => {
73
+ const listParams = new URLSearchParams();
74
+ listParams.set('page', String(page));
75
+ listParams.set('per_page', String(INTERNAL_PAGE_SIZE));
76
+ if (branchValue)
77
+ listParams.set('branch', branchValue);
78
+ if (flags['obj-type'])
79
+ listParams.set('obj_type', flags['obj-type']);
80
+ const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
81
+ headers: {
82
+ accept: 'application/json',
83
+ Authorization: `Bearer ${profile.access_token}`,
84
+ },
85
+ method: 'GET',
86
+ }, flags.verbose, profile.access_token);
87
+ if (!listResponse.ok) {
88
+ const errorText = await listResponse.text();
89
+ this.error(`Failed to list unit tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
90
+ }
91
+ return normalizeListResponse(await listResponse.json());
92
+ }, {
93
+ onTruncate: (collected) => {
94
+ this.warn(`Stopped after ${collected} unit tests — the list did not terminate. Some tests may not have run.`);
70
95
  },
71
- method: 'GET',
72
- }, flags.verbose, profile.access_token);
73
- if (!listResponse.ok) {
74
- const errorText = await listResponse.text();
75
- this.error(`Failed to list unit tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
76
- }
77
- const data = (await listResponse.json());
78
- let tests;
79
- if (Array.isArray(data)) {
80
- tests = data;
81
- }
82
- else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
83
- tests = data.items;
84
- }
85
- else {
86
- this.error('Unexpected API response format');
87
- }
96
+ });
88
97
  if (tests.length === 0) {
89
98
  this.log('No unit tests found');
90
99
  return;
@@ -93,7 +102,15 @@ Results: 4 passed, 1 failed
93
102
  this.log(`Running ${tests.length} unit test${tests.length === 1 ? '' : 's'}...\n`);
94
103
  }
95
104
  const results = [];
96
- for (const test of tests) {
105
+ const runOne = async (test) => {
106
+ // Shadowed accumulators: the body below is the original sequential
107
+ // logic, but it records output instead of printing it so a concurrent
108
+ // run can still emit in input order.
109
+ const results = [];
110
+ const lines = [];
111
+ const log = (message) => {
112
+ lines.push(message);
113
+ };
97
114
  const runUrl = `${baseUrl}/${test.id}/run`;
98
115
  try {
99
116
  const runResponse = await this.verboseFetch(runUrl, {
@@ -115,10 +132,10 @@ Results: 4 passed, 1 failed
115
132
  status: 'fail',
116
133
  });
117
134
  if (flags.output === 'summary') {
118
- this.log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
119
- this.log(` Error: API error ${runResponse.status}`);
135
+ log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
136
+ log(` Error: API error ${runResponse.status}`);
120
137
  }
121
- continue;
138
+ return { lines, results };
122
139
  }
123
140
  const runResult = (await runResponse.json());
124
141
  const passed = runResult.status === 'ok';
@@ -132,13 +149,13 @@ Results: 4 passed, 1 failed
132
149
  });
133
150
  if (flags.output === 'summary') {
134
151
  if (passed) {
135
- this.log(`PASS ${test.name} [${test.obj_type}: ${test.obj_name}]`);
152
+ log(`PASS ${test.name} [${test.obj_type}: ${test.obj_name}]`);
136
153
  }
137
154
  else {
138
- this.log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
155
+ log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
139
156
  for (const expect of failedExpects) {
140
157
  if (expect.message) {
141
- this.log(` Error: ${expect.message}`);
158
+ log(` Error: ${expect.message}`);
142
159
  }
143
160
  }
144
161
  }
@@ -154,11 +171,24 @@ Results: 4 passed, 1 failed
154
171
  status: 'fail',
155
172
  });
156
173
  if (flags.output === 'summary') {
157
- this.log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
158
- this.log(` Error: ${message}`);
174
+ log(`FAIL ${test.name} [${test.obj_type}: ${test.obj_name}]`);
175
+ log(` Error: ${message}`);
159
176
  }
160
177
  }
161
- }
178
+ return { lines, results };
179
+ };
180
+ // Print in input order as each test settles, so concurrent output stays
181
+ // diffable against a sequential run.
182
+ const emitter = createOrderedEmitter((settled) => {
183
+ for (const line of settled.lines)
184
+ this.log(line);
185
+ });
186
+ const perTest = await mapWithConcurrency(tests, flags.concurrency, async (test, index) => {
187
+ const settled = await runOne(test);
188
+ emitter.settle(index, settled);
189
+ return settled;
190
+ });
191
+ results.push(...perTest.flatMap((r) => r.results));
162
192
  const passed = results.filter((r) => r.status === 'pass').length;
163
193
  const failed = results.filter((r) => r.status === 'fail').length;
164
194
  if (flags.output === 'json') {