@xano/cli 1.0.6-beta.2 → 1.0.6-beta.20

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 (112) hide show
  1. package/README.md +217 -6
  2. package/dist/base-command.d.ts +5 -0
  3. package/dist/base-command.js +61 -4
  4. package/dist/commands/branch/list/index.js +5 -1
  5. package/dist/commands/ephemeral/create/index.js +3 -3
  6. package/dist/commands/ephemeral/list/index.js +2 -1
  7. package/dist/commands/ephemeral/pull/index.d.ts +0 -6
  8. package/dist/commands/ephemeral/pull/index.js +10 -95
  9. package/dist/commands/ephemeral/push/index.d.ts +10 -0
  10. package/dist/commands/ephemeral/push/index.js +77 -0
  11. package/dist/commands/ephemeral/static_host/build/list/index.d.ts +2 -3
  12. package/dist/commands/ephemeral/static_host/build/list/index.js +19 -40
  13. package/dist/commands/ephemeral/static_host/list/index.d.ts +2 -3
  14. package/dist/commands/ephemeral/static_host/list/index.js +20 -40
  15. package/dist/commands/flatten/index.d.ts +34 -0
  16. package/dist/commands/flatten/index.js +140 -0
  17. package/dist/commands/function/list/index.d.ts +4 -4
  18. package/dist/commands/function/list/index.js +30 -38
  19. package/dist/commands/function/run/index.d.ts +1 -0
  20. package/dist/commands/function/run/index.js +19 -5
  21. package/dist/commands/knowledge/list/index.js +2 -1
  22. package/dist/commands/platform/list/index.js +13 -14
  23. package/dist/commands/release/list/index.js +13 -14
  24. package/dist/commands/release/pull/index.d.ts +0 -6
  25. package/dist/commands/release/pull/index.js +10 -95
  26. package/dist/commands/sandbox/env/list/index.js +4 -0
  27. package/dist/commands/sandbox/pull/index.d.ts +0 -1
  28. package/dist/commands/sandbox/pull/index.js +10 -77
  29. package/dist/commands/sandbox/unit_test/list/index.d.ts +2 -0
  30. package/dist/commands/sandbox/unit_test/list/index.js +9 -14
  31. package/dist/commands/sandbox/unit_test/run/index.js +17 -11
  32. package/dist/commands/sandbox/unit_test/run_all/index.d.ts +1 -0
  33. package/dist/commands/sandbox/unit_test/run_all/index.js +76 -38
  34. package/dist/commands/sandbox/workflow_test/list/index.d.ts +2 -0
  35. package/dist/commands/sandbox/workflow_test/list/index.js +9 -14
  36. package/dist/commands/sandbox/workflow_test/run/index.js +12 -9
  37. package/dist/commands/sandbox/workflow_test/run_all/index.d.ts +1 -0
  38. package/dist/commands/sandbox/workflow_test/run_all/index.js +74 -36
  39. package/dist/commands/static_host/build/list/index.d.ts +2 -3
  40. package/dist/commands/static_host/build/list/index.js +19 -40
  41. package/dist/commands/static_host/build/pull/index.js +12 -9
  42. package/dist/commands/static_host/list/index.d.ts +2 -3
  43. package/dist/commands/static_host/list/index.js +20 -40
  44. package/dist/commands/static_host/migrate/index.js +14 -10
  45. package/dist/commands/tenant/backup/list/index.js +13 -14
  46. package/dist/commands/tenant/cluster/list/index.js +9 -10
  47. package/dist/commands/tenant/create/index.d.ts +3 -0
  48. package/dist/commands/tenant/create/index.js +21 -0
  49. package/dist/commands/tenant/deploy_release/index.d.ts +21 -0
  50. package/dist/commands/tenant/deploy_release/index.js +94 -2
  51. package/dist/commands/tenant/edit/index.d.ts +2 -0
  52. package/dist/commands/tenant/edit/index.js +39 -4
  53. package/dist/commands/tenant/env/list/index.js +4 -0
  54. package/dist/commands/tenant/list/index.js +19 -20
  55. package/dist/commands/tenant/pull/index.d.ts +0 -6
  56. package/dist/commands/tenant/pull/index.js +10 -106
  57. package/dist/commands/tenant/snapshot/list/index.js +5 -1
  58. package/dist/commands/tenant/unit_test/list/index.d.ts +4 -2
  59. package/dist/commands/tenant/unit_test/list/index.js +9 -14
  60. package/dist/commands/tenant/unit_test/run/index.js +19 -11
  61. package/dist/commands/tenant/unit_test/run_all/index.d.ts +1 -0
  62. package/dist/commands/tenant/unit_test/run_all/index.js +80 -38
  63. package/dist/commands/tenant/workflow_test/list/index.d.ts +4 -2
  64. package/dist/commands/tenant/workflow_test/list/index.js +9 -14
  65. package/dist/commands/tenant/workflow_test/run/index.js +14 -9
  66. package/dist/commands/tenant/workflow_test/run_all/index.d.ts +1 -0
  67. package/dist/commands/tenant/workflow_test/run_all/index.js +78 -36
  68. package/dist/commands/tenant_deploy_request/bypass/index.d.ts +20 -0
  69. package/dist/commands/tenant_deploy_request/bypass/index.js +79 -0
  70. package/dist/commands/tenant_deploy_request/create/index.d.ts +21 -0
  71. package/dist/commands/tenant_deploy_request/create/index.js +108 -0
  72. package/dist/commands/tenant_deploy_request/edit/index.d.ts +22 -0
  73. package/dist/commands/tenant_deploy_request/edit/index.js +103 -0
  74. package/dist/commands/tenant_deploy_request/get/index.d.ts +19 -0
  75. package/dist/commands/tenant_deploy_request/get/index.js +75 -0
  76. package/dist/commands/tenant_deploy_request/list/index.d.ts +20 -0
  77. package/dist/commands/tenant_deploy_request/list/index.js +122 -0
  78. package/dist/commands/tenant_deploy_request/revision/index.d.ts +21 -0
  79. package/dist/commands/tenant_deploy_request/revision/index.js +83 -0
  80. package/dist/commands/tenant_deploy_request/set_status/index.d.ts +21 -0
  81. package/dist/commands/tenant_deploy_request/set_status/index.js +112 -0
  82. package/dist/commands/unit_test/list/index.d.ts +3 -1
  83. package/dist/commands/unit_test/list/index.js +9 -14
  84. package/dist/commands/unit_test/run/index.js +19 -11
  85. package/dist/commands/unit_test/run_all/index.d.ts +1 -0
  86. package/dist/commands/unit_test/run_all/index.js +81 -41
  87. package/dist/commands/workflow_test/list/index.d.ts +3 -1
  88. package/dist/commands/workflow_test/list/index.js +10 -13
  89. package/dist/commands/workflow_test/run/index.js +15 -10
  90. package/dist/commands/workflow_test/run_all/index.d.ts +1 -0
  91. package/dist/commands/workflow_test/run_all/index.js +82 -39
  92. package/dist/commands/workspace/git/pull/index.js +48 -7
  93. package/dist/commands/workspace/list/index.js +7 -13
  94. package/dist/commands/workspace/pull/index.d.ts +0 -6
  95. package/dist/commands/workspace/pull/index.js +10 -115
  96. package/dist/utils/concurrency.d.ts +29 -0
  97. package/dist/utils/concurrency.js +54 -0
  98. package/dist/utils/document-parser.d.ts +90 -0
  99. package/dist/utils/document-parser.js +273 -1
  100. package/dist/utils/flatten.d.ts +36 -0
  101. package/dist/utils/flatten.js +68 -0
  102. package/dist/utils/knowledge-sync.d.ts +1 -0
  103. package/dist/utils/knowledge-sync.js +11 -5
  104. package/dist/utils/microservice-wait.d.ts +53 -0
  105. package/dist/utils/microservice-wait.js +83 -0
  106. package/dist/utils/multidoc-push.d.ts +88 -0
  107. package/dist/utils/multidoc-push.js +377 -8
  108. package/dist/utils/paging.d.ts +145 -0
  109. package/dist/utils/paging.js +199 -0
  110. package/dist/utils/reference-checker.js +13 -0
  111. package/oclif.manifest.json +4999 -3973
  112. package/package.json +1 -1
@@ -17,6 +17,8 @@ export default class FunctionRun extends BaseCommand {
17
17
  `$ xano function:run calcScore --json @payload.json --data env=staging`,
18
18
  `$ echo '{"email":"jo@x.com"}' | xano function:run calcScore --stdin -o json | jq .result`,
19
19
  `$ xano function:run calcScore --branch dev --logs`,
20
+ `$ xano function:run calcScore --datasource test
21
+ # Runs against the 'test' data source instead of 'live'`,
20
22
  ];
21
23
  static flags = {
22
24
  ...BaseCommand.baseFlags,
@@ -30,6 +32,10 @@ export default class FunctionRun extends BaseCommand {
30
32
  multiple: true,
31
33
  required: false,
32
34
  }),
35
+ datasource: Flags.string({
36
+ description: 'Data source to run against, e.g. test (defaults to live)',
37
+ required: false,
38
+ }),
33
39
  json: Flags.string({
34
40
  description: "Input as a JSON object: inline, @file.json, or '-' for stdin",
35
41
  exclusive: ['stdin'],
@@ -106,13 +112,21 @@ export default class FunctionRun extends BaseCommand {
106
112
  const body = { input, name: functionName };
107
113
  if (branch)
108
114
  body.branch = branch;
115
+ const headers = {
116
+ 'accept': 'application/json',
117
+ 'Authorization': `Bearer ${profile.access_token}`,
118
+ 'Content-Type': 'application/json',
119
+ };
120
+ // The Metadata API resolves the workspace's tables against the data source
121
+ // named by X-Data-Source (defaulting to `live`), so the run reads and writes
122
+ // the same tables a request carrying that header would. An unknown label is
123
+ // rejected server-side with "Invalid data source."
124
+ const dataSource = flags.datasource?.trim();
125
+ if (dataSource)
126
+ headers['X-Data-Source'] = dataSource;
109
127
  const response = await this.verboseFetch(apiUrl, {
110
128
  body: JSON.stringify(body),
111
- headers: {
112
- 'accept': 'application/json',
113
- 'Authorization': `Bearer ${profile.access_token}`,
114
- 'Content-Type': 'application/json',
115
- },
129
+ headers,
116
130
  method: 'POST',
117
131
  }, flags.verbose, profile.access_token);
118
132
  if (!response.ok) {
@@ -1,5 +1,6 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../base-command.js';
3
+ import { buildPagingJson } from '../../../utils/paging.js';
3
4
  export default class KnowledgeList extends BaseCommand {
4
5
  static args = {};
5
6
  static description = 'List workspace knowledge and skills as plain-text markdown';
@@ -92,7 +93,7 @@ export default class KnowledgeList extends BaseCommand {
92
93
  const data = await response.json();
93
94
  const items = Array.isArray(data) ? data : (data.items ?? []);
94
95
  if (flags.output === 'json') {
95
- this.log(JSON.stringify(items, null, 2));
96
+ this.log(JSON.stringify(buildPagingJson({ items }, { tier: 'none' }), null, 2));
96
97
  return;
97
98
  }
98
99
  this.log(this.formatMarkdown(items));
@@ -1,5 +1,6 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../base-command.js';
3
+ import { buildPagingJson } from '../../../utils/paging.js';
3
4
  export default class PlatformList extends BaseCommand {
4
5
  static description = 'List all platforms';
5
6
  static examples = [
@@ -47,22 +48,20 @@ Platforms:
47
48
  this.error('Unexpected API response format');
48
49
  }
49
50
  if (flags.output === 'json') {
50
- this.log(JSON.stringify(platforms, null, 2));
51
+ this.log(JSON.stringify(buildPagingJson({ items: platforms }, { tier: 'none' }), null, 2));
52
+ }
53
+ else if (platforms.length === 0) {
54
+ this.log('No platforms found');
51
55
  }
52
56
  else {
53
- if (platforms.length === 0) {
54
- this.log('No platforms found');
55
- }
56
- else {
57
- this.log('Platforms:');
58
- for (const platform of platforms) {
59
- const label = platform.name || `ID: ${platform.id}`;
60
- const helmTag = platform.helm?.tag ? ` | Helm: ${platform.helm.tag}` : '';
61
- const created = platform.created_at
62
- ? ` | Created: ${new Date(platform.created_at).toISOString().split('T')[0]}`
63
- : '';
64
- this.log(` ${label}${helmTag}${created}`);
65
- }
57
+ this.log('Platforms:');
58
+ for (const platform of platforms) {
59
+ const label = platform.name || `ID: ${platform.id}`;
60
+ const helmTag = platform.helm?.tag ? ` | Helm: ${platform.helm.tag}` : '';
61
+ const created = platform.created_at
62
+ ? ` | Created: ${new Date(platform.created_at).toISOString().split('T')[0]}`
63
+ : '';
64
+ this.log(` ${label}${helmTag}${created}`);
66
65
  }
67
66
  }
68
67
  }
@@ -1,5 +1,6 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../base-command.js';
3
+ import { buildPagingJson } from '../../../utils/paging.js';
3
4
  export default class ReleaseList extends BaseCommand {
4
5
  static description = 'List all releases in a workspace';
5
6
  static examples = [
@@ -57,22 +58,20 @@ Releases in workspace 5:
57
58
  this.error('Unexpected API response format');
58
59
  }
59
60
  if (flags.output === 'json') {
60
- this.log(JSON.stringify(releases, null, 2));
61
+ this.log(JSON.stringify(buildPagingJson({ items: releases }, { tier: 'none' }), null, 2));
62
+ }
63
+ else if (releases.length === 0) {
64
+ this.log('No releases found');
61
65
  }
62
66
  else {
63
- if (releases.length === 0) {
64
- this.log('No releases found');
65
- }
66
- else {
67
- this.log(`Releases in workspace ${workspaceId}:`);
68
- for (const release of releases) {
69
- const branch = release.branch ? ` - ${release.branch}` : '';
70
- const hotfix = release.hotfix ? ' [hotfix]' : '';
71
- const createdAt = release.created_at
72
- ? ` (${new Date(release.created_at).toLocaleString(undefined, { timeZoneName: 'short' })})`
73
- : '';
74
- this.log(` - ${release.name} (ID: ${release.id})${branch}${hotfix}${createdAt}`);
75
- }
67
+ this.log(`Releases in workspace ${workspaceId}:`);
68
+ for (const release of releases) {
69
+ const branch = release.branch ? ` - ${release.branch}` : '';
70
+ const hotfix = release.hotfix ? ' [hotfix]' : '';
71
+ const createdAt = release.created_at
72
+ ? ` (${new Date(release.created_at).toLocaleString(undefined, { timeZoneName: 'short' })})`
73
+ : '';
74
+ this.log(` - ${release.name} (ID: ${release.id})${branch}${hotfix}${createdAt}`);
76
75
  }
77
76
  }
78
77
  }
@@ -14,10 +14,4 @@ export default class ReleasePull extends BaseCommand {
14
14
  };
15
15
  run(): Promise<void>;
16
16
  private resolveReleaseName;
17
- /**
18
- * Sanitize a document name for use as a filename.
19
- * Strips quotes, replaces spaces with underscores, and removes
20
- * characters that are unsafe in filenames.
21
- */
22
- private sanitizeFilename;
23
17
  }
@@ -3,7 +3,7 @@ import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
4
  import snakeCase from 'lodash.snakecase';
5
5
  import BaseCommand from '../../../base-command.js';
6
- import { buildApiGroupFolderResolver, parseDocument } from '../../../utils/document-parser.js';
6
+ import { buildApiGroupFolderResolver, buildChannelServerResolver, parseDocument, resolveDocumentPath, } from '../../../utils/document-parser.js';
7
7
  export default class ReleasePull extends BaseCommand {
8
8
  static description = 'Pull a release multidoc from the Xano Metadata API and split into individual files';
9
9
  static examples = [
@@ -123,96 +123,19 @@ Pulled 58 documents from release 'v1.0'
123
123
  fs.mkdirSync(outputDir, { recursive: true });
124
124
  // Resolve api_group names to unique folder names, disambiguating collisions
125
125
  const getApiGroupFolder = buildApiGroupFolderResolver(documents, snakeCase);
126
+ // Resolve a realtime v2 channel path -> owning realtime_server name, so
127
+ // messages can nest under their channel's server (see resolver docs).
128
+ const getChannelServer = buildChannelServerResolver(documents);
126
129
  // Track filenames per type to handle duplicates
127
130
  const filenameCounters = new Map();
128
131
  let writtenCount = 0;
129
132
  for (const doc of documents) {
130
- let typeDir;
131
- let baseName;
132
- if (doc.type === 'workspace') {
133
- // workspace → workspace/{name}.xs
134
- typeDir = path.join(outputDir, 'workspace');
135
- baseName = this.sanitizeFilename(doc.name);
136
- }
137
- else if (doc.type === 'workspace_trigger') {
138
- // workspace_trigger → workspace/trigger/{name}.xs
139
- typeDir = path.join(outputDir, 'workspace', 'trigger');
140
- baseName = this.sanitizeFilename(doc.name);
141
- }
142
- else if (doc.type === 'error_trigger') {
143
- // error_trigger → workspace/trigger/{name}.xs (singleton, colocated with workspace triggers)
144
- typeDir = path.join(outputDir, 'workspace', 'trigger');
145
- baseName = this.sanitizeFilename(doc.name);
146
- }
147
- else if (doc.type === 'agent') {
148
- // agent → ai/agent/{name}.xs
149
- typeDir = path.join(outputDir, 'ai', 'agent');
150
- baseName = this.sanitizeFilename(doc.name);
151
- }
152
- else if (doc.type === 'mcp_server') {
153
- // mcp_server → ai/mcp_server/{name}.xs
154
- typeDir = path.join(outputDir, 'ai', 'mcp_server');
155
- baseName = this.sanitizeFilename(doc.name);
156
- }
157
- else if (doc.type === 'tool') {
158
- // tool → ai/tool/{name}.xs
159
- typeDir = path.join(outputDir, 'ai', 'tool');
160
- baseName = this.sanitizeFilename(doc.name);
161
- }
162
- else if (doc.type === 'agent_trigger') {
163
- // agent_trigger → ai/agent/trigger/{name}.xs
164
- typeDir = path.join(outputDir, 'ai', 'agent', 'trigger');
165
- baseName = this.sanitizeFilename(doc.name);
166
- }
167
- else if (doc.type === 'mcp_server_trigger') {
168
- // mcp_server_trigger → ai/mcp_server/trigger/{name}.xs
169
- typeDir = path.join(outputDir, 'ai', 'mcp_server', 'trigger');
170
- baseName = this.sanitizeFilename(doc.name);
171
- }
172
- else if (doc.type === 'table_trigger') {
173
- // table_trigger → table/trigger/{name}.xs
174
- typeDir = path.join(outputDir, 'table', 'trigger');
175
- baseName = this.sanitizeFilename(doc.name);
176
- }
177
- else if (doc.type === 'realtime_channel') {
178
- // realtime_channel → realtime/channel/{name}.xs
179
- typeDir = path.join(outputDir, 'realtime', 'channel');
180
- baseName = this.sanitizeFilename(doc.name);
181
- }
182
- else if (doc.type === 'realtime_trigger') {
183
- // realtime_trigger → realtime/trigger/{name}.xs
184
- typeDir = path.join(outputDir, 'realtime', 'trigger');
185
- baseName = this.sanitizeFilename(doc.name);
186
- }
187
- else if (doc.type === 'api_group') {
188
- // api_group "test" → api/{resolved_folder}/{name}.xs
189
- const groupFolder = getApiGroupFolder(doc.name);
190
- typeDir = path.join(outputDir, 'api', groupFolder);
191
- baseName = this.sanitizeFilename(doc.name);
192
- }
193
- else if (doc.type === 'query' && doc.apiGroup) {
194
- // query in group "test" → api/{resolved_folder}/{query_name}.xs
195
- const groupFolder = getApiGroupFolder(doc.apiGroup);
196
- const nameParts = doc.name.split('/');
197
- const leafName = nameParts.pop();
198
- const folderParts = nameParts.map((part) => snakeCase(part));
199
- typeDir = path.join(outputDir, 'api', groupFolder, ...folderParts);
200
- baseName = this.sanitizeFilename(leafName);
201
- if (doc.verb) {
202
- baseName = `${baseName}_${doc.verb}`;
203
- }
204
- }
205
- else {
206
- // Default: split folder path from name
207
- const nameParts = doc.name.split('/');
208
- const leafName = nameParts.pop();
209
- const folderParts = nameParts.map((part) => snakeCase(part));
210
- typeDir = path.join(outputDir, doc.type, ...folderParts);
211
- baseName = this.sanitizeFilename(leafName);
212
- if (doc.verb) {
213
- baseName = `${baseName}_${doc.verb}`;
214
- }
215
- }
133
+ const { baseName, typeDir } = resolveDocumentPath(doc, outputDir, {
134
+ getApiGroupFolder,
135
+ getChannelServer,
136
+ join: path.join,
137
+ snakeCase,
138
+ });
216
139
  fs.mkdirSync(typeDir, { recursive: true });
217
140
  // Track duplicates per directory
218
141
  const dirKey = path.relative(outputDir, typeDir);
@@ -257,12 +180,4 @@ Pulled 58 documents from release 'v1.0'
257
180
  }
258
181
  return match.id;
259
182
  }
260
- /**
261
- * Sanitize a document name for use as a filename.
262
- * Strips quotes, replaces spaces with underscores, and removes
263
- * characters that are unsafe in filenames.
264
- */
265
- sanitizeFilename(name) {
266
- return snakeCase(name.replaceAll('"', ''));
267
- }
268
183
  }
@@ -1,5 +1,6 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../../base-command.js';
3
+ import { formatPagingFooter } from '../../../../utils/paging.js';
3
4
  export default class SandboxEnvList extends BaseCommand {
4
5
  static description = 'List environment variable keys for a sandbox environment';
5
6
  static examples = [
@@ -51,6 +52,9 @@ Environment variables for sandbox environment:
51
52
  this.log(` - ${envVar.name}`);
52
53
  }
53
54
  }
55
+ const footer = formatPagingFooter({ items: envVars }, { noun: 'environment variable', tier: 'none' });
56
+ if (footer)
57
+ this.log(footer);
54
58
  }
55
59
  }
56
60
  catch (error) {
@@ -12,5 +12,4 @@ export default class SandboxPull extends BaseCommand {
12
12
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
13
  };
14
14
  run(): Promise<void>;
15
- private sanitizeFilename;
16
15
  }
@@ -1,7 +1,7 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import snakeCase from 'lodash.snakecase';
3
3
  import BaseCommand from '../../../base-command.js';
4
- import { buildApiGroupFolderResolver, parseDocument } from '../../../utils/document-parser.js';
4
+ import { buildApiGroupFolderResolver, buildChannelServerResolver, parseDocument, resolveDocumentPath, } from '../../../utils/document-parser.js';
5
5
  import { fetchKnowledge, writeKnowledge } from '../../../utils/knowledge-sync.js';
6
6
  import * as fs from 'node:fs';
7
7
  import * as path from 'node:path';
@@ -92,82 +92,18 @@ Pulled 42 documents from sandbox environment to ./my-sandbox
92
92
  const outputDir = path.resolve(flags.directory);
93
93
  fs.mkdirSync(outputDir, { recursive: true });
94
94
  const getApiGroupFolder = buildApiGroupFolderResolver(documents, snakeCase);
95
+ // Resolve a realtime v2 channel path -> owning realtime_server name, so
96
+ // messages can nest under their channel's server (see resolver docs).
97
+ const getChannelServer = buildChannelServerResolver(documents);
95
98
  const filenameCounters = new Map();
96
99
  let writtenCount = 0;
97
100
  for (const doc of documents) {
98
- let typeDir;
99
- let baseName;
100
- if (doc.type === 'workspace') {
101
- typeDir = path.join(outputDir, 'workspace');
102
- baseName = this.sanitizeFilename(doc.name);
103
- }
104
- else if (doc.type === 'workspace_trigger') {
105
- typeDir = path.join(outputDir, 'workspace', 'trigger');
106
- baseName = this.sanitizeFilename(doc.name);
107
- }
108
- else if (doc.type === 'error_trigger') {
109
- // error_trigger → workspace/trigger/{name}.xs (singleton, colocated with workspace triggers)
110
- typeDir = path.join(outputDir, 'workspace', 'trigger');
111
- baseName = this.sanitizeFilename(doc.name);
112
- }
113
- else if (doc.type === 'agent') {
114
- typeDir = path.join(outputDir, 'ai', 'agent');
115
- baseName = this.sanitizeFilename(doc.name);
116
- }
117
- else if (doc.type === 'mcp_server') {
118
- typeDir = path.join(outputDir, 'ai', 'mcp_server');
119
- baseName = this.sanitizeFilename(doc.name);
120
- }
121
- else if (doc.type === 'tool') {
122
- typeDir = path.join(outputDir, 'ai', 'tool');
123
- baseName = this.sanitizeFilename(doc.name);
124
- }
125
- else if (doc.type === 'agent_trigger') {
126
- typeDir = path.join(outputDir, 'ai', 'agent', 'trigger');
127
- baseName = this.sanitizeFilename(doc.name);
128
- }
129
- else if (doc.type === 'mcp_server_trigger') {
130
- typeDir = path.join(outputDir, 'ai', 'mcp_server', 'trigger');
131
- baseName = this.sanitizeFilename(doc.name);
132
- }
133
- else if (doc.type === 'table_trigger') {
134
- typeDir = path.join(outputDir, 'table', 'trigger');
135
- baseName = this.sanitizeFilename(doc.name);
136
- }
137
- else if (doc.type === 'realtime_channel') {
138
- typeDir = path.join(outputDir, 'realtime', 'channel');
139
- baseName = this.sanitizeFilename(doc.name);
140
- }
141
- else if (doc.type === 'realtime_trigger') {
142
- typeDir = path.join(outputDir, 'realtime', 'trigger');
143
- baseName = this.sanitizeFilename(doc.name);
144
- }
145
- else if (doc.type === 'api_group') {
146
- const groupFolder = getApiGroupFolder(doc.name);
147
- typeDir = path.join(outputDir, 'api', groupFolder);
148
- baseName = this.sanitizeFilename(doc.name);
149
- }
150
- else if (doc.type === 'query' && doc.apiGroup) {
151
- const groupFolder = getApiGroupFolder(doc.apiGroup);
152
- const nameParts = doc.name.split('/');
153
- const leafName = nameParts.pop();
154
- const folderParts = nameParts.map((part) => snakeCase(part));
155
- typeDir = path.join(outputDir, 'api', groupFolder, ...folderParts);
156
- baseName = this.sanitizeFilename(leafName);
157
- if (doc.verb) {
158
- baseName = `${baseName}_${doc.verb}`;
159
- }
160
- }
161
- else {
162
- const nameParts = doc.name.split('/');
163
- const leafName = nameParts.pop();
164
- const folderParts = nameParts.map((part) => snakeCase(part));
165
- typeDir = path.join(outputDir, doc.type, ...folderParts);
166
- baseName = this.sanitizeFilename(leafName);
167
- if (doc.verb) {
168
- baseName = `${baseName}_${doc.verb}`;
169
- }
170
- }
101
+ const { baseName, typeDir } = resolveDocumentPath(doc, outputDir, {
102
+ getApiGroupFolder,
103
+ getChannelServer,
104
+ join: path.join,
105
+ snakeCase,
106
+ });
171
107
  fs.mkdirSync(typeDir, { recursive: true });
172
108
  const dirKey = path.relative(outputDir, typeDir);
173
109
  if (!filenameCounters.has(dirKey)) {
@@ -194,7 +130,4 @@ Pulled 42 documents from sandbox environment to ./my-sandbox
194
130
  parts.push(`${knowledgeCount} knowledge file${knowledgeCount === 1 ? '' : 's'}`);
195
131
  this.log(`Pulled ${parts.join(' + ')} from sandbox environment to ${flags.directory}`);
196
132
  }
197
- sanitizeFilename(name) {
198
- return snakeCase(name.replaceAll('"', ''));
199
- }
200
133
  }
@@ -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) {
@@ -46,24 +46,30 @@ Result: PASS
46
46
  this.error(message);
47
47
  }
48
48
  const result = (await response.json());
49
+ // Anything other than 'ok' is a failure -- the API has been observed
50
+ // returning several distinct non-ok statuses, so never enumerate them.
51
+ const passed = result.status === 'ok';
49
52
  if (flags.output === 'json') {
50
53
  this.log(JSON.stringify(result, null, 2));
51
54
  }
55
+ else if (passed) {
56
+ this.log('Result: PASS');
57
+ }
52
58
  else {
53
- if (result.status === 'ok') {
54
- this.log('Result: PASS');
55
- }
56
- else {
57
- this.log('Result: FAIL');
58
- const failedExpects = result.results?.filter((r) => r.status === 'fail') ?? [];
59
- for (const expect of failedExpects) {
60
- if (expect.message) {
61
- this.log(` Error: ${expect.message}`);
62
- }
59
+ this.log('Result: FAIL');
60
+ const failedExpects = result.results?.filter((r) => r.status === 'fail') ?? [];
61
+ for (const expect of failedExpects) {
62
+ if (expect.message) {
63
+ this.log(` Error: ${expect.message}`);
63
64
  }
64
- this.exit(1);
65
65
  }
66
66
  }
67
+ // Set the code rather than throwing via this.exit(): the exit must apply
68
+ // in both output modes, and process.exitCode cannot be swallowed by the
69
+ // surrounding catch. Mirrors what run_all already does.
70
+ if (!passed) {
71
+ process.exitCode = 1;
72
+ }
67
73
  }
68
74
  catch (error) {
69
75
  if (error instanceof Error && 'oclif' in 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>;