@xano/cli 1.1.0 → 1.3.0

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 (128) hide show
  1. package/README.md +155 -4
  2. package/dist/base-command.d.ts +43 -36
  3. package/dist/base-command.js +223 -160
  4. package/dist/commands/auth/index.d.ts +1 -1
  5. package/dist/commands/auth/index.js +7 -7
  6. package/dist/commands/branch/create/index.d.ts +1 -1
  7. package/dist/commands/branch/create/index.js +1 -1
  8. package/dist/commands/ephemeral/create/index.d.ts +18 -0
  9. package/dist/commands/ephemeral/create/index.js +103 -0
  10. package/dist/commands/ephemeral/delete/index.d.ts +18 -0
  11. package/dist/commands/ephemeral/delete/index.js +99 -0
  12. package/dist/commands/ephemeral/edit/index.d.ts +18 -0
  13. package/dist/commands/ephemeral/edit/index.js +109 -0
  14. package/dist/commands/ephemeral/get/index.d.ts +21 -0
  15. package/dist/commands/ephemeral/get/index.js +125 -0
  16. package/dist/commands/ephemeral/impersonate/index.d.ts +20 -0
  17. package/dist/commands/ephemeral/impersonate/index.js +120 -0
  18. package/dist/commands/ephemeral/list/index.d.ts +19 -0
  19. package/dist/commands/ephemeral/list/index.js +123 -0
  20. package/dist/commands/ephemeral/pull/index.d.ts +19 -0
  21. package/dist/commands/ephemeral/pull/index.js +163 -0
  22. package/dist/commands/ephemeral/push/index.d.ts +35 -0
  23. package/dist/commands/ephemeral/push/index.js +223 -0
  24. package/dist/commands/ephemeral/static_host/build/delete/index.d.ts +20 -0
  25. package/dist/commands/ephemeral/static_host/build/delete/index.js +121 -0
  26. package/dist/commands/ephemeral/static_host/build/get/index.d.ts +18 -0
  27. package/dist/commands/ephemeral/static_host/build/get/index.js +137 -0
  28. package/dist/commands/ephemeral/static_host/build/list/index.d.ts +18 -0
  29. package/dist/commands/ephemeral/static_host/build/list/index.js +137 -0
  30. package/dist/commands/ephemeral/static_host/build/pull/index.d.ts +53 -0
  31. package/dist/commands/ephemeral/static_host/build/pull/index.js +308 -0
  32. package/dist/commands/ephemeral/static_host/build/push/index.d.ts +24 -0
  33. package/dist/commands/ephemeral/static_host/build/push/index.js +250 -0
  34. package/dist/commands/ephemeral/static_host/create/index.d.ts +18 -0
  35. package/dist/commands/ephemeral/static_host/create/index.js +92 -0
  36. package/dist/commands/ephemeral/static_host/deploy/index.d.ts +19 -0
  37. package/dist/commands/ephemeral/static_host/deploy/index.js +112 -0
  38. package/dist/commands/ephemeral/static_host/edit/index.d.ts +24 -0
  39. package/dist/commands/ephemeral/static_host/edit/index.js +158 -0
  40. package/dist/commands/ephemeral/static_host/get/index.d.ts +19 -0
  41. package/dist/commands/ephemeral/static_host/get/index.js +101 -0
  42. package/dist/commands/ephemeral/static_host/list/index.d.ts +17 -0
  43. package/dist/commands/ephemeral/static_host/list/index.js +132 -0
  44. package/dist/commands/flatten/index.d.ts +34 -0
  45. package/dist/commands/flatten/index.js +140 -0
  46. package/dist/commands/function/create/index.js +1 -1
  47. package/dist/commands/function/edit/index.js +26 -32
  48. package/dist/commands/function/get/index.js +2 -3
  49. package/dist/commands/function/run/index.d.ts +1 -0
  50. package/dist/commands/function/run/index.js +19 -6
  51. package/dist/commands/profile/create/index.d.ts +1 -1
  52. package/dist/commands/profile/create/index.js +7 -7
  53. package/dist/commands/profile/me/index.js +1 -1
  54. package/dist/commands/profile/use/index.js +0 -4
  55. package/dist/commands/profile/wizard/index.d.ts +1 -1
  56. package/dist/commands/profile/wizard/index.js +12 -10
  57. package/dist/commands/profile/workspace/set/index.js +1 -1
  58. package/dist/commands/release/edit/index.js +2 -2
  59. package/dist/commands/release/export/index.js +1 -2
  60. package/dist/commands/release/import/index.js +1 -1
  61. package/dist/commands/release/pull/index.d.ts +0 -6
  62. package/dist/commands/release/pull/index.js +13 -99
  63. package/dist/commands/release/push/index.js +2 -2
  64. package/dist/commands/sandbox/env/get_all/index.js +1 -1
  65. package/dist/commands/sandbox/env/set_all/index.js +1 -1
  66. package/dist/commands/sandbox/get/index.js +1 -1
  67. package/dist/commands/sandbox/license/get/index.js +1 -1
  68. package/dist/commands/sandbox/license/set/index.js +1 -1
  69. package/dist/commands/sandbox/pull/index.d.ts +0 -1
  70. package/dist/commands/sandbox/pull/index.js +13 -81
  71. package/dist/commands/sandbox/push/index.d.ts +1 -1
  72. package/dist/commands/sandbox/push/index.js +5 -5
  73. package/dist/commands/sandbox/unit_test/run/index.js +17 -11
  74. package/dist/commands/sandbox/unit_test/run_all/index.js +9 -1
  75. package/dist/commands/sandbox/workflow_test/run/index.js +12 -9
  76. package/dist/commands/sandbox/workflow_test/run_all/index.js +9 -1
  77. package/dist/commands/static_host/build/create/index.d.ts +1 -1
  78. package/dist/commands/static_host/build/create/index.js +2 -2
  79. package/dist/commands/static_host/build/pull/index.js +1 -1
  80. package/dist/commands/static_host/build/push/index.js +1 -1
  81. package/dist/commands/static_host/edit/index.js +1 -1
  82. package/dist/commands/tenant/backup/export/index.js +1 -2
  83. package/dist/commands/tenant/backup/import/index.js +1 -1
  84. package/dist/commands/tenant/cluster/create/index.js +1 -1
  85. package/dist/commands/tenant/cluster/license/get/index.js +1 -1
  86. package/dist/commands/tenant/cluster/license/set/index.js +1 -1
  87. package/dist/commands/tenant/create/index.d.ts +1 -1
  88. package/dist/commands/tenant/create/index.js +6 -6
  89. package/dist/commands/tenant/deploy_platform/index.js +1 -1
  90. package/dist/commands/tenant/edit/index.js +7 -7
  91. package/dist/commands/tenant/env/get_all/index.js +1 -1
  92. package/dist/commands/tenant/env/set_all/index.js +1 -1
  93. package/dist/commands/tenant/impersonate/index.d.ts +1 -1
  94. package/dist/commands/tenant/impersonate/index.js +14 -14
  95. package/dist/commands/tenant/license/get/index.js +1 -1
  96. package/dist/commands/tenant/license/set/index.js +1 -1
  97. package/dist/commands/tenant/pull/index.d.ts +0 -6
  98. package/dist/commands/tenant/pull/index.js +14 -100
  99. package/dist/commands/tenant/unit_test/run/index.js +19 -11
  100. package/dist/commands/tenant/unit_test/run_all/index.js +13 -1
  101. package/dist/commands/tenant/workflow_test/run/index.js +14 -9
  102. package/dist/commands/tenant/workflow_test/run_all/index.js +13 -1
  103. package/dist/commands/unit_test/run/index.js +19 -11
  104. package/dist/commands/unit_test/run_all/index.js +13 -1
  105. package/dist/commands/workflow_test/run/index.js +15 -10
  106. package/dist/commands/workflow_test/run_all/index.js +15 -3
  107. package/dist/commands/workspace/git/pull/index.d.ts +4 -4
  108. package/dist/commands/workspace/git/pull/index.js +158 -97
  109. package/dist/commands/workspace/pull/index.d.ts +1 -7
  110. package/dist/commands/workspace/pull/index.js +17 -103
  111. package/dist/help.js +4 -2
  112. package/dist/update-check.d.ts +1 -1
  113. package/dist/update-check.js +1 -1
  114. package/dist/utils/document-parser.d.ts +116 -0
  115. package/dist/utils/document-parser.js +311 -4
  116. package/dist/utils/flatten.d.ts +36 -0
  117. package/dist/utils/flatten.js +68 -0
  118. package/dist/utils/knowledge-sync.d.ts +1 -0
  119. package/dist/utils/knowledge-sync.js +11 -6
  120. package/dist/utils/microservice-wait.d.ts +53 -0
  121. package/dist/utils/microservice-wait.js +84 -0
  122. package/dist/utils/multidoc-push.d.ts +88 -0
  123. package/dist/utils/multidoc-push.js +378 -15
  124. package/dist/utils/paging.d.ts +2 -1
  125. package/dist/utils/paging.js +0 -1
  126. package/dist/utils/reference-checker.js +14 -3
  127. package/oclif.manifest.json +5898 -3804
  128. package/package.json +10 -1
@@ -1,6 +1,6 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import * as fs from 'node:fs';
3
- import * as path from 'node:path';
3
+ import path from 'node:path';
4
4
  import BaseCommand from '../../../../base-command.js';
5
5
  export default class TenantClusterCreate extends BaseCommand {
6
6
  static description = 'Create a new tenant cluster';
@@ -1,6 +1,6 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import * as fs from 'node:fs';
3
- import * as path from 'node:path';
3
+ import path from 'node:path';
4
4
  import BaseCommand from '../../../../../base-command.js';
5
5
  export default class TenantClusterLicenseGet extends BaseCommand {
6
6
  static args = {
@@ -1,6 +1,6 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import * as fs from 'node:fs';
3
- import * as path from 'node:path';
3
+ import path from 'node:path';
4
4
  import BaseCommand from '../../../../../base-command.js';
5
5
  export default class TenantClusterLicenseSet extends BaseCommand {
6
6
  static args = {
@@ -13,8 +13,8 @@ export default class TenantCreate extends BaseCommand {
13
13
  license: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
14
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
15
15
  platform_id: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
- type: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
17
16
  tasks: import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
+ type: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
18
18
  workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
19
  config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
20
  profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -1,7 +1,7 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import * as yaml from 'js-yaml';
3
3
  import * as fs from 'node:fs';
4
- import * as path from 'node:path';
4
+ import path from 'node:path';
5
5
  import BaseCommand from '../../../base-command.js';
6
6
  export default class TenantCreate extends BaseCommand {
7
7
  static args = {
@@ -54,17 +54,17 @@ Created tenant: Production (production) - ID: 42
54
54
  description: 'Platform ID to use',
55
55
  required: false,
56
56
  }),
57
+ tasks: Flags.boolean({
58
+ allowNo: true,
59
+ default: true,
60
+ description: 'Enable background tasks',
61
+ }),
57
62
  type: Flags.string({
58
63
  default: 'tier1',
59
64
  description: 'Tenant type',
60
65
  options: ['tier1', 'tier2', 'tier3'],
61
66
  required: false,
62
67
  }),
63
- tasks: Flags.boolean({
64
- allowNo: true,
65
- default: true,
66
- description: 'Enable background tasks',
67
- }),
68
68
  workspace: Flags.string({
69
69
  char: 'w',
70
70
  description: 'Workspace ID (uses profile workspace if not provided)',
@@ -1,6 +1,6 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import * as fs from 'node:fs';
3
- import * as path from 'node:path';
3
+ import path from 'node:path';
4
4
  import BaseCommand from '../../../base-command.js';
5
5
  export default class TenantDeployPlatform extends BaseCommand {
6
6
  static args = {
@@ -91,16 +91,16 @@ Updated tenant: New Name (my-tenant) - ID: 42
91
91
  const current = await getResponse.json();
92
92
  // Merge in user-provided values
93
93
  const body = {
94
- description: flags.description !== undefined ? flags.description : (current.description ?? ''),
95
- display: flags.display !== undefined ? flags.display : (current.display ?? current.name),
96
- domain: flags.domain !== undefined ? flags.domain : (current.domain ?? ''),
97
- ingress: flags.ingress !== undefined ? flags.ingress : (current.ingress ?? true),
98
- proxy: flags.proxy !== undefined ? flags.proxy : (current.proxy ?? ''),
94
+ description: flags.description === undefined ? (current.description ?? '') : flags.description,
95
+ display: flags.display === undefined ? (current.display ?? current.name) : flags.display,
96
+ domain: flags.domain === undefined ? (current.domain ?? '') : flags.domain,
97
+ ingress: flags.ingress === undefined ? (current.ingress ?? true) : flags.ingress,
98
+ proxy: flags.proxy === undefined ? (current.proxy ?? '') : flags.proxy,
99
99
  rbac: {
100
- enabled: flags.rbac !== undefined ? flags.rbac : (current.rbac?.enabled ?? false),
100
+ enabled: flags.rbac === undefined ? (current.rbac?.enabled ?? false) : flags.rbac,
101
101
  },
102
102
  tag: current.tag ?? [],
103
- tasks: flags.tasks !== undefined ? flags.tasks : (current.tasks ?? true),
103
+ tasks: flags.tasks === undefined ? (current.tasks ?? true) : flags.tasks,
104
104
  };
105
105
  // Update tenant
106
106
  const putResponse = await this.verboseFetch(baseUrl, {
@@ -1,7 +1,7 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import * as yaml from 'js-yaml';
3
3
  import * as fs from 'node:fs';
4
- import * as path from 'node:path';
4
+ import path from 'node:path';
5
5
  import BaseCommand from '../../../../base-command.js';
6
6
  export default class TenantEnvGetAll extends BaseCommand {
7
7
  static args = {
@@ -1,7 +1,7 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import * as yaml from 'js-yaml';
3
3
  import * as fs from 'node:fs';
4
- import * as path from 'node:path';
4
+ import path from 'node:path';
5
5
  import BaseCommand from '../../../../base-command.js';
6
6
  export default class TenantEnvSetAll extends BaseCommand {
7
7
  static args = {
@@ -14,6 +14,6 @@ export default class TenantImpersonate extends BaseCommand {
14
14
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
15
  };
16
16
  run(): Promise<void>;
17
- private getImpersonateResponse;
18
17
  private getFrontendUrl;
18
+ private getImpersonateResponse;
19
19
  }
@@ -76,13 +76,26 @@ Impersonation successful!
76
76
  }
77
77
  }
78
78
  }
79
+ getFrontendUrl(instanceOrigin) {
80
+ try {
81
+ const url = new URL(instanceOrigin);
82
+ if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
83
+ url.port = '4200';
84
+ return url.origin;
85
+ }
86
+ }
87
+ catch {
88
+ // fall through
89
+ }
90
+ return instanceOrigin;
91
+ }
79
92
  async getImpersonateResponse(profile, workspaceId, tenantName) {
80
93
  const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${encodeURIComponent(tenantName)}/impersonate`;
81
94
  const { verbose } = await this.parse(TenantImpersonate).then((r) => r.flags);
82
95
  const response = await this.verboseFetch(apiUrl, {
83
96
  headers: {
84
- Authorization: `Bearer ${profile.access_token}`,
85
97
  accept: 'application/json',
98
+ Authorization: `Bearer ${profile.access_token}`,
86
99
  },
87
100
  method: 'GET',
88
101
  }, verbose, profile.access_token);
@@ -96,17 +109,4 @@ Impersonation successful!
96
109
  }
97
110
  return result;
98
111
  }
99
- getFrontendUrl(instanceOrigin) {
100
- try {
101
- const url = new URL(instanceOrigin);
102
- if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
103
- url.port = '4200';
104
- return url.origin;
105
- }
106
- }
107
- catch {
108
- // fall through
109
- }
110
- return instanceOrigin;
111
- }
112
112
  }
@@ -1,6 +1,6 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import * as fs from 'node:fs';
3
- import * as path from 'node:path';
3
+ import path from 'node:path';
4
4
  import BaseCommand from '../../../../base-command.js';
5
5
  export default class TenantLicenseGet extends BaseCommand {
6
6
  static args = {
@@ -1,6 +1,6 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import * as fs from 'node:fs';
3
- import * as path from 'node:path';
3
+ import path from 'node:path';
4
4
  import BaseCommand from '../../../../base-command.js';
5
5
  export default class TenantLicenseSet extends BaseCommand {
6
6
  static args = {
@@ -14,10 +14,4 @@ export default class Pull extends BaseCommand {
14
14
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
15
  };
16
16
  run(): Promise<void>;
17
- /**
18
- * Sanitize a document name for use as a filename.
19
- * Strips quotes, replaces spaces with underscores, and removes
20
- * characters that are unsafe in filenames.
21
- */
22
- private sanitizeFilename;
23
17
  }
@@ -1,9 +1,9 @@
1
1
  import { Flags } from '@oclif/core';
2
- import * as fs from 'node:fs';
3
- import * as path from 'node:path';
4
2
  import snakeCase from 'lodash.snakecase';
3
+ import * as fs from 'node:fs';
4
+ import path from 'node:path';
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 Pull extends BaseCommand {
8
8
  static description = 'Pull a tenant multidoc from the Xano Metadata API and split into individual files';
9
9
  static examples = [
@@ -57,7 +57,7 @@ Pulled 58 documents from tenant my-tenant
57
57
  };
58
58
  async run() {
59
59
  const { flags } = await this.parse(Pull);
60
- const { profileName, profile } = this.resolveProfile(flags);
60
+ const { profile, profileName } = this.resolveProfile(flags);
61
61
  // Determine workspace_id from flag or profile
62
62
  let workspaceId;
63
63
  if (flags.workspace) {
@@ -129,96 +129,19 @@ Pulled 58 documents from tenant my-tenant
129
129
  fs.mkdirSync(outputDir, { recursive: true });
130
130
  // Resolve api_group names to unique folder names, disambiguating collisions
131
131
  const getApiGroupFolder = buildApiGroupFolderResolver(documents, snakeCase);
132
+ // Resolve a realtime v2 channel path -> owning realtime_server name, so
133
+ // messages can nest under their channel's server (see resolver docs).
134
+ const getChannelServer = buildChannelServerResolver(documents);
132
135
  // Track filenames per type to handle duplicates
133
136
  const filenameCounters = new Map();
134
137
  let writtenCount = 0;
135
138
  for (const doc of documents) {
136
- let typeDir;
137
- let baseName;
138
- if (doc.type === 'workspace') {
139
- // workspace → workspace/{name}.xs
140
- typeDir = path.join(outputDir, 'workspace');
141
- baseName = this.sanitizeFilename(doc.name);
142
- }
143
- else if (doc.type === 'workspace_trigger') {
144
- // workspace_trigger → workspace/trigger/{name}.xs
145
- typeDir = path.join(outputDir, 'workspace', 'trigger');
146
- baseName = this.sanitizeFilename(doc.name);
147
- }
148
- else if (doc.type === 'error_trigger') {
149
- // error_trigger → workspace/trigger/{name}.xs (singleton, colocated with workspace triggers)
150
- typeDir = path.join(outputDir, 'workspace', 'trigger');
151
- baseName = this.sanitizeFilename(doc.name);
152
- }
153
- else if (doc.type === 'agent') {
154
- // agent → ai/agent/{name}.xs
155
- typeDir = path.join(outputDir, 'ai', 'agent');
156
- baseName = this.sanitizeFilename(doc.name);
157
- }
158
- else if (doc.type === 'mcp_server') {
159
- // mcp_server → ai/mcp_server/{name}.xs
160
- typeDir = path.join(outputDir, 'ai', 'mcp_server');
161
- baseName = this.sanitizeFilename(doc.name);
162
- }
163
- else if (doc.type === 'tool') {
164
- // tool → ai/tool/{name}.xs
165
- typeDir = path.join(outputDir, 'ai', 'tool');
166
- baseName = this.sanitizeFilename(doc.name);
167
- }
168
- else if (doc.type === 'agent_trigger') {
169
- // agent_trigger → ai/agent/trigger/{name}.xs
170
- typeDir = path.join(outputDir, 'ai', 'agent', 'trigger');
171
- baseName = this.sanitizeFilename(doc.name);
172
- }
173
- else if (doc.type === 'mcp_server_trigger') {
174
- // mcp_server_trigger → ai/mcp_server/trigger/{name}.xs
175
- typeDir = path.join(outputDir, 'ai', 'mcp_server', 'trigger');
176
- baseName = this.sanitizeFilename(doc.name);
177
- }
178
- else if (doc.type === 'table_trigger') {
179
- // table_trigger → table/trigger/{name}.xs
180
- typeDir = path.join(outputDir, 'table', 'trigger');
181
- baseName = this.sanitizeFilename(doc.name);
182
- }
183
- else if (doc.type === 'realtime_channel') {
184
- // realtime_channel → realtime/channel/{name}.xs
185
- typeDir = path.join(outputDir, 'realtime', 'channel');
186
- baseName = this.sanitizeFilename(doc.name);
187
- }
188
- else if (doc.type === 'realtime_trigger') {
189
- // realtime_trigger → realtime/trigger/{name}.xs
190
- typeDir = path.join(outputDir, 'realtime', 'trigger');
191
- baseName = this.sanitizeFilename(doc.name);
192
- }
193
- else if (doc.type === 'api_group') {
194
- // api_group "test" → api/{resolved_folder}/{name}.xs
195
- const groupFolder = getApiGroupFolder(doc.name);
196
- typeDir = path.join(outputDir, 'api', groupFolder);
197
- baseName = this.sanitizeFilename(doc.name);
198
- }
199
- else if (doc.type === 'query' && doc.apiGroup) {
200
- // query in group "test" → api/{resolved_folder}/{query_name}.xs
201
- const groupFolder = getApiGroupFolder(doc.apiGroup);
202
- const nameParts = doc.name.split('/');
203
- const leafName = nameParts.pop();
204
- const folderParts = nameParts.map((part) => snakeCase(part));
205
- typeDir = path.join(outputDir, 'api', groupFolder, ...folderParts);
206
- baseName = this.sanitizeFilename(leafName);
207
- if (doc.verb) {
208
- baseName = `${baseName}_${doc.verb}`;
209
- }
210
- }
211
- else {
212
- // Default: split folder path from name
213
- const nameParts = doc.name.split('/');
214
- const leafName = nameParts.pop();
215
- const folderParts = nameParts.map((part) => snakeCase(part));
216
- typeDir = path.join(outputDir, doc.type, ...folderParts);
217
- baseName = this.sanitizeFilename(leafName);
218
- if (doc.verb) {
219
- baseName = `${baseName}_${doc.verb}`;
220
- }
221
- }
139
+ const { baseName, typeDir } = resolveDocumentPath(doc, outputDir, {
140
+ getApiGroupFolder,
141
+ getChannelServer,
142
+ join: path.join,
143
+ snakeCase,
144
+ });
222
145
  fs.mkdirSync(typeDir, { recursive: true });
223
146
  // Track duplicates per directory
224
147
  const dirKey = path.relative(outputDir, typeDir);
@@ -229,20 +152,11 @@ Pulled 58 documents from tenant my-tenant
229
152
  const count = typeCounters.get(baseName) || 0;
230
153
  typeCounters.set(baseName, count + 1);
231
154
  // Append numeric suffix for duplicates
232
- let filename;
233
- filename = count === 0 ? `${baseName}.xs` : `${baseName}_${count + 1}.xs`;
155
+ const filename = count === 0 ? `${baseName}.xs` : `${baseName}_${count + 1}.xs`;
234
156
  const filePath = path.join(typeDir, filename);
235
157
  fs.writeFileSync(filePath, doc.content, 'utf8');
236
158
  writtenCount++;
237
159
  }
238
160
  this.log(`Pulled ${writtenCount} documents from tenant ${tenantName} to ${flags.directory}`);
239
161
  }
240
- /**
241
- * Sanitize a document name for use as a filename.
242
- * Strips quotes, replaces spaces with underscores, and removes
243
- * characters that are unsafe in filenames.
244
- */
245
- sanitizeFilename(name) {
246
- return snakeCase(name.replaceAll('"', ''));
247
- }
248
162
  }
@@ -68,26 +68,34 @@ Result: PASS
68
68
  this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
69
69
  }
70
70
  const result = (await response.json());
71
+ // Anything other than 'ok' is a failure -- the API has been observed
72
+ // returning several distinct non-ok statuses, so never enumerate them.
73
+ const passed = result.status === 'ok';
71
74
  if (flags.output === 'json') {
72
75
  this.log(JSON.stringify(result, null, 2));
73
76
  }
77
+ else if (passed) {
78
+ this.log('Result: PASS');
79
+ }
74
80
  else {
75
- if (result.status === 'ok') {
76
- this.log('Result: PASS');
77
- }
78
- else {
79
- this.log('Result: FAIL');
80
- const failedExpects = result.results?.filter((r) => r.status === 'fail') ?? [];
81
- for (const expect of failedExpects) {
82
- if (expect.message) {
83
- this.log(` Error: ${expect.message}`);
84
- }
81
+ this.log('Result: FAIL');
82
+ const failedExpects = result.results?.filter((r) => r.status === 'fail') ?? [];
83
+ for (const expect of failedExpects) {
84
+ if (expect.message) {
85
+ this.log(` Error: ${expect.message}`);
85
86
  }
86
- this.exit(1);
87
87
  }
88
88
  }
89
+ // Set the code rather than throwing via this.exit(): the exit must apply
90
+ // in both output modes, and process.exitCode cannot be swallowed by the
91
+ // surrounding catch. Mirrors what run_all already does.
92
+ if (!passed) {
93
+ process.exitCode = 1;
94
+ }
89
95
  }
90
96
  catch (error) {
97
+ if (error instanceof Error && 'oclif' in error)
98
+ throw error;
91
99
  if (error instanceof Error) {
92
100
  this.error(`Failed to run unit test: ${error.message}`);
93
101
  }
@@ -95,7 +95,15 @@ Results: 4 passed, 1 failed
95
95
  },
96
96
  });
97
97
  if (tests.length === 0) {
98
- this.log('No unit tests found');
98
+ // An empty suite is a normal state (fresh branches have no tests) and
99
+ // is a success, not a failure. Under -o json it must still be parseable,
100
+ // with the same keys the populated path emits below.
101
+ if (flags.output === 'json') {
102
+ this.log(JSON.stringify({ failed: 0, passed: 0, results: [] }, null, 2));
103
+ }
104
+ else {
105
+ this.log('No unit tests found');
106
+ }
99
107
  return;
100
108
  }
101
109
  if (flags.output === 'summary') {
@@ -162,6 +170,8 @@ Results: 4 passed, 1 failed
162
170
  }
163
171
  }
164
172
  catch (error) {
173
+ if (error instanceof Error && 'oclif' in error)
174
+ throw error;
165
175
  const message = error instanceof Error ? error.message : String(error);
166
176
  results.push({
167
177
  message,
@@ -202,6 +212,8 @@ Results: 4 passed, 1 failed
202
212
  }
203
213
  }
204
214
  catch (error) {
215
+ if (error instanceof Error && 'oclif' in error)
216
+ throw error;
205
217
  if (error instanceof Error) {
206
218
  this.error(`Failed to run unit tests: ${error.message}`);
207
219
  }
@@ -61,24 +61,29 @@ Result: PASS (0.25s)
61
61
  this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
62
62
  }
63
63
  const result = (await response.json());
64
+ // Anything other than 'ok' is a failure -- the API has been observed
65
+ // returning several distinct non-ok statuses, so never enumerate them.
66
+ const passed = result.status === 'ok';
64
67
  if (flags.output === 'json') {
65
68
  this.log(JSON.stringify(result, null, 2));
66
69
  }
67
70
  else {
68
71
  const timing = result.timing ? ` (${result.timing}s)` : '';
69
- if (result.status === 'ok') {
70
- this.log(`Result: PASS${timing}`);
71
- }
72
- else {
73
- this.log(`Result: FAIL${timing}`);
74
- if (result.message) {
75
- this.log(` Error: ${result.message}`);
76
- }
77
- this.exit(1);
72
+ this.log(`Result: ${passed ? 'PASS' : 'FAIL'}${timing}`);
73
+ if (!passed && result.message) {
74
+ this.log(` Error: ${result.message}`);
78
75
  }
79
76
  }
77
+ // Set the code rather than throwing via this.exit(): the exit must apply
78
+ // in both output modes, and process.exitCode cannot be swallowed by the
79
+ // surrounding catch. Mirrors what run_all already does.
80
+ if (!passed) {
81
+ process.exitCode = 1;
82
+ }
80
83
  }
81
84
  catch (error) {
85
+ if (error instanceof Error && 'oclif' in error)
86
+ throw error;
82
87
  if (error instanceof Error) {
83
88
  this.error(`Failed to run workflow test: ${error.message}`);
84
89
  }
@@ -85,7 +85,15 @@ Results: 2 passed, 1 failed
85
85
  },
86
86
  });
87
87
  if (tests.length === 0) {
88
- this.log('No workflow tests found');
88
+ // An empty suite is a normal state (fresh branches have no tests) and
89
+ // is a success, not a failure. Under -o json it must still be parseable,
90
+ // with the same keys the populated path emits below.
91
+ if (flags.output === 'json') {
92
+ this.log(JSON.stringify({ failed: 0, passed: 0, results: [] }, null, 2));
93
+ }
94
+ else {
95
+ this.log('No workflow tests found');
96
+ }
89
97
  return;
90
98
  }
91
99
  if (flags.output === 'summary') {
@@ -146,6 +154,8 @@ Results: 2 passed, 1 failed
146
154
  }
147
155
  }
148
156
  catch (error) {
157
+ if (error instanceof Error && 'oclif' in error)
158
+ throw error;
149
159
  const message = error instanceof Error ? error.message : String(error);
150
160
  results.push({
151
161
  message,
@@ -184,6 +194,8 @@ Results: 2 passed, 1 failed
184
194
  }
185
195
  }
186
196
  catch (error) {
197
+ if (error instanceof Error && 'oclif' in error)
198
+ throw error;
187
199
  if (error instanceof Error) {
188
200
  this.error(`Failed to run workflow tests: ${error.message}`);
189
201
  }
@@ -63,26 +63,34 @@ Result: PASS
63
63
  this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
64
64
  }
65
65
  const result = await response.json();
66
+ // Anything other than 'ok' is a failure -- the API has been observed
67
+ // returning several distinct non-ok statuses, so never enumerate them.
68
+ const passed = result.status === 'ok';
66
69
  if (flags.output === 'json') {
67
70
  this.log(JSON.stringify(result, null, 2));
68
71
  }
72
+ else if (passed) {
73
+ this.log('Result: PASS');
74
+ }
69
75
  else {
70
- if (result.status === 'ok') {
71
- this.log('Result: PASS');
72
- }
73
- else {
74
- this.log('Result: FAIL');
75
- const failedExpects = result.results?.filter(r => r.status === 'fail') ?? [];
76
- for (const expect of failedExpects) {
77
- if (expect.message) {
78
- this.log(` Error: ${expect.message}`);
79
- }
76
+ this.log('Result: FAIL');
77
+ const failedExpects = result.results?.filter(r => r.status === 'fail') ?? [];
78
+ for (const expect of failedExpects) {
79
+ if (expect.message) {
80
+ this.log(` Error: ${expect.message}`);
80
81
  }
81
- this.exit(1);
82
82
  }
83
83
  }
84
+ // Set the code rather than throwing via this.exit(): the exit must apply
85
+ // in both output modes, and process.exitCode cannot be swallowed by the
86
+ // surrounding catch. Mirrors what run_all already does.
87
+ if (!passed) {
88
+ process.exitCode = 1;
89
+ }
84
90
  }
85
91
  catch (error) {
92
+ if (error instanceof Error && 'oclif' in error)
93
+ throw error;
86
94
  if (error instanceof Error) {
87
95
  this.error(`Failed to run unit test: ${error.message}`);
88
96
  }
@@ -91,7 +91,15 @@ Results: 2 passed, 1 failed
91
91
  },
92
92
  });
93
93
  if (tests.length === 0) {
94
- this.log('No unit tests found');
94
+ // An empty suite is a normal state (fresh branches have no tests) and
95
+ // is a success, not a failure. Under -o json it must still be parseable,
96
+ // with the same keys the populated path emits below.
97
+ if (flags.output === 'json') {
98
+ this.log(JSON.stringify({ failed: 0, passed: 0, results: [] }, null, 2));
99
+ }
100
+ else {
101
+ this.log('No unit tests found');
102
+ }
95
103
  return;
96
104
  }
97
105
  if (flags.output === 'summary') {
@@ -161,6 +169,8 @@ Results: 2 passed, 1 failed
161
169
  }
162
170
  }
163
171
  catch (error) {
172
+ if (error instanceof Error && 'oclif' in error)
173
+ throw error;
164
174
  const message = error instanceof Error ? error.message : String(error);
165
175
  results.push({
166
176
  message,
@@ -202,6 +212,8 @@ Results: 2 passed, 1 failed
202
212
  }
203
213
  }
204
214
  catch (error) {
215
+ if (error instanceof Error && 'oclif' in error)
216
+ throw error;
205
217
  if (error instanceof Error) {
206
218
  this.error(`Failed to run unit tests: ${error.message}`);
207
219
  }
@@ -55,24 +55,29 @@ Result: PASS (1.234s)
55
55
  this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
56
56
  }
57
57
  const result = await response.json();
58
+ // Anything other than 'ok' is a failure -- the API has been observed
59
+ // returning several distinct non-ok statuses, so never enumerate them.
60
+ const passed = result.status === 'ok';
58
61
  if (flags.output === 'json') {
59
62
  this.log(JSON.stringify(result, null, 2));
60
63
  }
61
64
  else {
62
- const timing = `(${result.timing.toFixed(3)}s)`;
63
- if (result.status === 'ok') {
64
- this.log(`Result: PASS ${timing}`);
65
- }
66
- else {
67
- this.log(`Result: FAIL ${timing}`);
68
- if (result.message) {
69
- this.log(` Error: ${result.message}`);
70
- }
71
- this.exit(1);
65
+ const timing = typeof result.timing === 'number' ? ` (${result.timing.toFixed(3)}s)` : '';
66
+ this.log(`Result: ${passed ? 'PASS' : 'FAIL'}${timing}`);
67
+ if (!passed && result.message) {
68
+ this.log(` Error: ${result.message}`);
72
69
  }
73
70
  }
71
+ // Set the code rather than throwing via this.exit(): the exit must apply
72
+ // in both output modes, and process.exitCode cannot be swallowed by the
73
+ // surrounding catch. Mirrors what run_all already does.
74
+ if (!passed) {
75
+ process.exitCode = 1;
76
+ }
74
77
  }
75
78
  catch (error) {
79
+ if (error instanceof Error && 'oclif' in error)
80
+ throw error;
76
81
  if (error instanceof Error) {
77
82
  this.error(`Failed to run workflow test: ${error.message}`);
78
83
  }