@xano/cli 0.0.94 → 0.0.95-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +28 -1
  2. package/dist/base-command.d.ts +25 -0
  3. package/dist/base-command.js +37 -0
  4. package/dist/commands/auth/index.js +1 -1
  5. package/dist/commands/profile/create/index.js +2 -2
  6. package/dist/commands/profile/edit/index.js +2 -2
  7. package/dist/commands/profile/me/index.js +21 -2
  8. package/dist/commands/profile/wizard/index.js +3 -3
  9. package/dist/commands/profile/workspace/set/index.js +1 -1
  10. package/dist/commands/release/deploy/index.d.ts +17 -0
  11. package/dist/commands/release/deploy/index.js +107 -0
  12. package/dist/commands/sandbox/env/delete/index.d.ts +14 -0
  13. package/dist/commands/sandbox/env/delete/index.js +87 -0
  14. package/dist/commands/sandbox/env/get/index.d.ts +12 -0
  15. package/dist/commands/sandbox/env/get/index.js +63 -0
  16. package/dist/commands/sandbox/env/get_all/index.d.ts +13 -0
  17. package/dist/commands/sandbox/env/get_all/index.js +76 -0
  18. package/dist/commands/sandbox/env/list/index.d.ts +11 -0
  19. package/dist/commands/sandbox/env/list/index.js +65 -0
  20. package/dist/commands/sandbox/env/set/index.d.ts +13 -0
  21. package/dist/commands/sandbox/env/set/index.js +72 -0
  22. package/dist/commands/sandbox/env/set_all/index.d.ts +13 -0
  23. package/dist/commands/sandbox/env/set_all/index.js +84 -0
  24. package/dist/commands/sandbox/get/index.d.ts +11 -0
  25. package/dist/commands/sandbox/get/index.js +61 -0
  26. package/dist/commands/sandbox/impersonate/index.d.ts +5 -0
  27. package/dist/commands/sandbox/impersonate/index.js +5 -0
  28. package/dist/commands/sandbox/license/get/index.d.ts +13 -0
  29. package/dist/commands/sandbox/license/get/index.js +76 -0
  30. package/dist/commands/sandbox/license/set/index.d.ts +14 -0
  31. package/dist/commands/sandbox/license/set/index.js +93 -0
  32. package/dist/commands/sandbox/pull/index.d.ts +17 -0
  33. package/dist/commands/sandbox/pull/index.js +180 -0
  34. package/dist/commands/sandbox/push/index.d.ts +18 -0
  35. package/dist/commands/sandbox/push/index.js +141 -0
  36. package/dist/commands/sandbox/reset/index.d.ts +12 -0
  37. package/dist/commands/sandbox/reset/index.js +69 -0
  38. package/dist/commands/sandbox/review/index.d.ts +13 -0
  39. package/dist/commands/sandbox/review/index.js +92 -0
  40. package/dist/commands/sandbox/unit_test/list/index.d.ts +13 -0
  41. package/dist/commands/sandbox/unit_test/list/index.js +89 -0
  42. package/dist/commands/sandbox/unit_test/run/index.d.ts +14 -0
  43. package/dist/commands/sandbox/unit_test/run/index.js +77 -0
  44. package/dist/commands/sandbox/unit_test/run_all/index.d.ts +13 -0
  45. package/dist/commands/sandbox/unit_test/run_all/index.js +167 -0
  46. package/dist/commands/sandbox/workflow_test/delete/index.d.ts +17 -0
  47. package/dist/commands/sandbox/workflow_test/delete/index.js +59 -0
  48. package/dist/commands/sandbox/workflow_test/get/index.d.ts +17 -0
  49. package/dist/commands/sandbox/workflow_test/get/index.js +58 -0
  50. package/dist/commands/sandbox/workflow_test/list/index.d.ts +12 -0
  51. package/dist/commands/sandbox/workflow_test/list/index.js +82 -0
  52. package/dist/commands/sandbox/workflow_test/run/index.d.ts +17 -0
  53. package/dist/commands/sandbox/workflow_test/run/index.js +75 -0
  54. package/dist/commands/sandbox/workflow_test/run_all/index.d.ts +12 -0
  55. package/dist/commands/sandbox/workflow_test/run_all/index.js +153 -0
  56. package/dist/commands/tenant/create/index.d.ts +2 -2
  57. package/dist/commands/tenant/create/index.js +23 -11
  58. package/dist/commands/tenant/get/index.js +2 -2
  59. package/dist/commands/tenant/list/index.js +2 -2
  60. package/dist/commands/tenant/push/index.js +0 -34
  61. package/dist/commands/tenant/unit_test/list/index.d.ts +15 -0
  62. package/dist/commands/tenant/unit_test/list/index.js +140 -0
  63. package/dist/commands/tenant/unit_test/run/index.d.ts +16 -0
  64. package/dist/commands/tenant/unit_test/run/index.js +128 -0
  65. package/dist/commands/tenant/unit_test/run_all/index.d.ts +15 -0
  66. package/dist/commands/tenant/unit_test/run_all/index.js +215 -0
  67. package/dist/commands/tenant/workflow_test/delete/index.d.ts +19 -0
  68. package/dist/commands/tenant/workflow_test/delete/index.js +110 -0
  69. package/dist/commands/tenant/workflow_test/get/index.d.ts +19 -0
  70. package/dist/commands/tenant/workflow_test/get/index.js +112 -0
  71. package/dist/commands/tenant/workflow_test/list/index.d.ts +14 -0
  72. package/dist/commands/tenant/workflow_test/list/index.js +133 -0
  73. package/dist/commands/tenant/workflow_test/run/index.d.ts +19 -0
  74. package/dist/commands/tenant/workflow_test/run/index.js +126 -0
  75. package/dist/commands/tenant/workflow_test/run_all/index.d.ts +14 -0
  76. package/dist/commands/tenant/workflow_test/run_all/index.js +201 -0
  77. package/dist/commands/workspace/edit/index.d.ts +1 -0
  78. package/dist/commands/workspace/edit/index.js +16 -6
  79. package/dist/commands/workspace/get/index.js +9 -7
  80. package/dist/commands/workspace/list/index.d.ts +1 -0
  81. package/dist/commands/workspace/list/index.js +14 -7
  82. package/dist/commands/workspace/push/index.js +30 -2
  83. package/dist/help.d.ts +2 -1
  84. package/dist/help.js +39 -1
  85. package/oclif.manifest.json +4701 -2272
  86. package/package.json +17 -2
@@ -0,0 +1,126 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import BaseCommand from '../../../../base-command.js';
3
+ export default class TenantWorkflowTestRun extends BaseCommand {
4
+ static args = {
5
+ workflow_test_id: Args.integer({
6
+ description: 'ID of the workflow test to run',
7
+ required: true,
8
+ }),
9
+ };
10
+ static description = 'Run a workflow test for a tenant';
11
+ static examples = [
12
+ `$ xano tenant workflow-test run 42 -t my-tenant
13
+ Running workflow test 42...
14
+ Result: PASS (0.25s)
15
+ `,
16
+ `$ xano tenant workflow-test run 42 -t my-tenant -o json`,
17
+ ];
18
+ static flags = {
19
+ ...BaseCommand.baseFlags,
20
+ output: Flags.string({
21
+ char: 'o',
22
+ default: 'summary',
23
+ description: 'Output format',
24
+ options: ['summary', 'json'],
25
+ required: false,
26
+ }),
27
+ tenant: Flags.string({
28
+ char: 't',
29
+ description: 'Tenant name',
30
+ required: true,
31
+ }),
32
+ workspace: Flags.string({
33
+ char: 'w',
34
+ description: 'Workspace ID (uses profile workspace if not provided)',
35
+ required: false,
36
+ }),
37
+ };
38
+ async run() {
39
+ const { args, flags } = await this.parse(TenantWorkflowTestRun);
40
+ const profileName = flags.profile || this.getDefaultProfile();
41
+ const credentials = this.loadCredentialsFile();
42
+ if (!credentials || !(profileName in credentials.profiles)) {
43
+ this.error(`Profile '${profileName}' not found.\nCreate a profile using 'xano profile create'`);
44
+ }
45
+ const profile = credentials.profiles[profileName];
46
+ if (!profile.instance_origin) {
47
+ this.error(`Profile '${profileName}' is missing instance_origin`);
48
+ }
49
+ if (!profile.access_token) {
50
+ this.error(`Profile '${profileName}' is missing access_token`);
51
+ }
52
+ const workspaceId = flags.workspace || profile.workspace;
53
+ if (!workspaceId) {
54
+ this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
55
+ }
56
+ // Resolve tenant to get its workspace
57
+ const tenantUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${encodeURIComponent(flags.tenant)}`;
58
+ let tenantWorkspaceId;
59
+ try {
60
+ const tenantResponse = await this.verboseFetch(tenantUrl, {
61
+ headers: {
62
+ accept: 'application/json',
63
+ Authorization: `Bearer ${profile.access_token}`,
64
+ },
65
+ method: 'GET',
66
+ }, flags.verbose, profile.access_token);
67
+ if (!tenantResponse.ok) {
68
+ const errorText = await tenantResponse.text();
69
+ this.error(`Failed to find tenant '${flags.tenant}': ${tenantResponse.status}\n${errorText}`);
70
+ }
71
+ const tenant = (await tenantResponse.json());
72
+ tenantWorkspaceId = String(tenant.workspace?.id || workspaceId);
73
+ }
74
+ catch (error) {
75
+ if (error instanceof Error) {
76
+ this.error(`Failed to resolve tenant: ${error.message}`);
77
+ }
78
+ else {
79
+ this.error(`Failed to resolve tenant: ${String(error)}`);
80
+ }
81
+ }
82
+ const apiUrl = `${profile.instance_origin}/api:meta/workspace/${tenantWorkspaceId}/workflow_test/${args.workflow_test_id}/run`;
83
+ try {
84
+ if (flags.output === 'summary') {
85
+ this.log(`Running workflow test ${args.workflow_test_id}...`);
86
+ }
87
+ const response = await this.verboseFetch(apiUrl, {
88
+ headers: {
89
+ accept: 'application/json',
90
+ Authorization: `Bearer ${profile.access_token}`,
91
+ 'Content-Type': 'application/json',
92
+ },
93
+ method: 'POST',
94
+ }, flags.verbose, profile.access_token);
95
+ if (!response.ok) {
96
+ const errorText = await response.text();
97
+ this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
98
+ }
99
+ const result = (await response.json());
100
+ if (flags.output === 'json') {
101
+ this.log(JSON.stringify(result, null, 2));
102
+ }
103
+ else {
104
+ const timing = result.timing ? ` (${result.timing}s)` : '';
105
+ if (result.status === 'ok') {
106
+ this.log(`Result: PASS${timing}`);
107
+ }
108
+ else {
109
+ this.log(`Result: FAIL${timing}`);
110
+ if (result.message) {
111
+ this.log(` Error: ${result.message}`);
112
+ }
113
+ this.exit(1);
114
+ }
115
+ }
116
+ }
117
+ catch (error) {
118
+ if (error instanceof Error) {
119
+ this.error(`Failed to run workflow test: ${error.message}`);
120
+ }
121
+ else {
122
+ this.error(`Failed to run workflow test: ${String(error)}`);
123
+ }
124
+ }
125
+ }
126
+ }
@@ -0,0 +1,14 @@
1
+ import BaseCommand from '../../../../base-command.js';
2
+ export default class TenantWorkflowTestRunAll extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
+ tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
+ workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ };
13
+ run(): Promise<void>;
14
+ }
@@ -0,0 +1,201 @@
1
+ import { Flags } from '@oclif/core';
2
+ import BaseCommand from '../../../../base-command.js';
3
+ export default class TenantWorkflowTestRunAll extends BaseCommand {
4
+ static description = 'Run all workflow tests for a tenant';
5
+ static examples = [
6
+ `$ xano tenant workflow-test run-all -t my-tenant
7
+ Running 3 workflow tests...
8
+
9
+ PASS my-test (0.25s)
10
+ FAIL data-check (0.10s)
11
+ Error: assertion failed
12
+
13
+ Results: 2 passed, 1 failed
14
+ `,
15
+ `$ xano tenant workflow-test run-all -t my-tenant -o json`,
16
+ ];
17
+ static flags = {
18
+ ...BaseCommand.baseFlags,
19
+ branch: Flags.string({
20
+ char: 'b',
21
+ description: 'Filter by branch name',
22
+ required: false,
23
+ }),
24
+ output: Flags.string({
25
+ char: 'o',
26
+ default: 'summary',
27
+ description: 'Output format',
28
+ options: ['summary', 'json'],
29
+ required: false,
30
+ }),
31
+ tenant: Flags.string({
32
+ char: 't',
33
+ description: 'Tenant name',
34
+ required: true,
35
+ }),
36
+ workspace: Flags.string({
37
+ char: 'w',
38
+ description: 'Workspace ID (uses profile workspace if not provided)',
39
+ required: false,
40
+ }),
41
+ };
42
+ async run() {
43
+ const { flags } = await this.parse(TenantWorkflowTestRunAll);
44
+ const profileName = flags.profile || this.getDefaultProfile();
45
+ const credentials = this.loadCredentialsFile();
46
+ if (!credentials || !(profileName in credentials.profiles)) {
47
+ this.error(`Profile '${profileName}' not found.\nCreate a profile using 'xano profile create'`);
48
+ }
49
+ const profile = credentials.profiles[profileName];
50
+ if (!profile.instance_origin) {
51
+ this.error(`Profile '${profileName}' is missing instance_origin`);
52
+ }
53
+ if (!profile.access_token) {
54
+ this.error(`Profile '${profileName}' is missing access_token`);
55
+ }
56
+ const workspaceId = flags.workspace || profile.workspace;
57
+ if (!workspaceId) {
58
+ this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
59
+ }
60
+ // Resolve tenant to get its workspace
61
+ const tenantUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${encodeURIComponent(flags.tenant)}`;
62
+ let tenantWorkspaceId;
63
+ try {
64
+ const tenantResponse = await this.verboseFetch(tenantUrl, {
65
+ headers: {
66
+ accept: 'application/json',
67
+ Authorization: `Bearer ${profile.access_token}`,
68
+ },
69
+ method: 'GET',
70
+ }, flags.verbose, profile.access_token);
71
+ if (!tenantResponse.ok) {
72
+ const errorText = await tenantResponse.text();
73
+ this.error(`Failed to find tenant '${flags.tenant}': ${tenantResponse.status}\n${errorText}`);
74
+ }
75
+ const tenant = (await tenantResponse.json());
76
+ tenantWorkspaceId = String(tenant.workspace?.id || workspaceId);
77
+ }
78
+ catch (error) {
79
+ if (error instanceof Error) {
80
+ this.error(`Failed to resolve tenant: ${error.message}`);
81
+ }
82
+ else {
83
+ this.error(`Failed to resolve tenant: ${String(error)}`);
84
+ }
85
+ }
86
+ const baseUrl = `${profile.instance_origin}/api:meta/workspace/${tenantWorkspaceId}/workflow_test`;
87
+ try {
88
+ const listParams = new URLSearchParams();
89
+ listParams.set('per_page', '10000');
90
+ if (flags.branch)
91
+ listParams.set('branch', flags.branch);
92
+ const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
93
+ headers: {
94
+ accept: 'application/json',
95
+ Authorization: `Bearer ${profile.access_token}`,
96
+ },
97
+ method: 'GET',
98
+ }, flags.verbose, profile.access_token);
99
+ if (!listResponse.ok) {
100
+ const errorText = await listResponse.text();
101
+ this.error(`Failed to list workflow tests: ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
102
+ }
103
+ const data = (await listResponse.json());
104
+ let tests;
105
+ if (Array.isArray(data)) {
106
+ tests = data;
107
+ }
108
+ else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
109
+ tests = data.items;
110
+ }
111
+ else {
112
+ this.error('Unexpected API response format');
113
+ }
114
+ if (tests.length === 0) {
115
+ this.log('No workflow tests found');
116
+ return;
117
+ }
118
+ if (flags.output === 'summary') {
119
+ this.log(`Running ${tests.length} workflow test${tests.length === 1 ? '' : 's'}...\n`);
120
+ }
121
+ const results = [];
122
+ for (const test of tests) {
123
+ const runUrl = `${baseUrl}/${test.id}/run`;
124
+ try {
125
+ const runResponse = await this.verboseFetch(runUrl, {
126
+ headers: {
127
+ accept: 'application/json',
128
+ Authorization: `Bearer ${profile.access_token}`,
129
+ 'Content-Type': 'application/json',
130
+ },
131
+ method: 'POST',
132
+ }, flags.verbose, profile.access_token);
133
+ if (!runResponse.ok) {
134
+ const errorText = await runResponse.text();
135
+ results.push({
136
+ message: `API error ${runResponse.status}: ${errorText}`,
137
+ name: test.name,
138
+ status: 'fail',
139
+ });
140
+ if (flags.output === 'summary') {
141
+ this.log(`FAIL ${test.name}`);
142
+ this.log(` Error: API error ${runResponse.status}`);
143
+ }
144
+ continue;
145
+ }
146
+ const runResult = (await runResponse.json());
147
+ const passed = runResult.status === 'ok';
148
+ results.push({
149
+ message: runResult.message,
150
+ name: test.name,
151
+ status: passed ? 'pass' : 'fail',
152
+ timing: runResult.timing,
153
+ });
154
+ if (flags.output === 'summary') {
155
+ const timing = runResult.timing ? ` (${runResult.timing}s)` : '';
156
+ if (passed) {
157
+ this.log(`PASS ${test.name}${timing}`);
158
+ }
159
+ else {
160
+ this.log(`FAIL ${test.name}${timing}`);
161
+ if (runResult.message) {
162
+ this.log(` Error: ${runResult.message}`);
163
+ }
164
+ }
165
+ }
166
+ }
167
+ catch (error) {
168
+ const message = error instanceof Error ? error.message : String(error);
169
+ results.push({
170
+ message,
171
+ name: test.name,
172
+ status: 'fail',
173
+ });
174
+ if (flags.output === 'summary') {
175
+ this.log(`FAIL ${test.name}`);
176
+ this.log(` Error: ${message}`);
177
+ }
178
+ }
179
+ }
180
+ const passed = results.filter((r) => r.status === 'pass').length;
181
+ const failed = results.filter((r) => r.status === 'fail').length;
182
+ if (flags.output === 'json') {
183
+ this.log(JSON.stringify({ failed, passed, results }, null, 2));
184
+ }
185
+ else {
186
+ this.log(`\nResults: ${passed} passed, ${failed} failed`);
187
+ }
188
+ if (failed > 0) {
189
+ process.exitCode = 1;
190
+ }
191
+ }
192
+ catch (error) {
193
+ if (error instanceof Error) {
194
+ this.error(`Failed to run workflow tests: ${error.message}`);
195
+ }
196
+ else {
197
+ this.error(`Failed to run workflow tests: ${String(error)}`);
198
+ }
199
+ }
200
+ }
201
+ }
@@ -9,6 +9,7 @@ export default class WorkspaceEdit extends BaseCommand {
9
9
  static description: string;
10
10
  static examples: string[];
11
11
  static flags: {
12
+ 'allow-push': import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
13
  description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
14
  name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
15
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
@@ -35,6 +35,11 @@ Updated workspace: my-workspace (ID: 123)
35
35
  ];
36
36
  static flags = {
37
37
  ...BaseCommand.baseFlags,
38
+ 'allow-push': Flags.boolean({
39
+ allowNo: true,
40
+ description: 'Enable or disable direct CLI push to this workspace (not applicable on Free plan)',
41
+ required: false,
42
+ }),
38
43
  description: Flags.string({
39
44
  char: 'd',
40
45
  description: 'New description for the workspace',
@@ -102,9 +107,12 @@ Updated workspace: my-workspace (ID: 123)
102
107
  if (flags['require-token'] !== undefined) {
103
108
  body.documentation = { require_token: flags['require-token'] };
104
109
  }
110
+ if (flags['allow-push'] !== undefined) {
111
+ body.preferences = { allow_push: flags['allow-push'] };
112
+ }
105
113
  // Check if at least one field is being updated
106
114
  if (Object.keys(body).length === 0) {
107
- this.error('No fields specified to update. Use --name, --description, --swagger, or --require-token flags.\n' +
115
+ this.error('No fields specified to update. Use --name, --description, --swagger, --require-token, or --allow-push flags.\n' +
108
116
  'Example: xano workspace edit 123 --name "new-name"');
109
117
  }
110
118
  // Construct the API URL
@@ -114,8 +122,8 @@ Updated workspace: my-workspace (ID: 123)
114
122
  const response = await this.verboseFetch(apiUrl, {
115
123
  body: JSON.stringify(body),
116
124
  headers: {
117
- 'accept': 'application/json',
118
- 'Authorization': `Bearer ${profile.access_token}`,
125
+ accept: 'application/json',
126
+ Authorization: `Bearer ${profile.access_token}`,
119
127
  'Content-Type': 'application/json',
120
128
  },
121
129
  method: 'PUT',
@@ -124,7 +132,7 @@ Updated workspace: my-workspace (ID: 123)
124
132
  const errorText = await response.text();
125
133
  this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
126
134
  }
127
- const workspace = await response.json();
135
+ const workspace = (await response.json());
128
136
  // Output results
129
137
  if (flags.output === 'json') {
130
138
  this.log(JSON.stringify(workspace, null, 2));
@@ -141,6 +149,9 @@ Updated workspace: my-workspace (ID: 123)
141
149
  if (workspace.documentation?.require_token !== undefined) {
142
150
  this.log(` Require Token: ${workspace.documentation.require_token}`);
143
151
  }
152
+ if (workspace.preferences?.allow_push !== undefined) {
153
+ this.log(` Allow Push: ${workspace.preferences.allow_push}`);
154
+ }
144
155
  }
145
156
  }
146
157
  catch (error) {
@@ -157,8 +168,7 @@ Updated workspace: my-workspace (ID: 123)
157
168
  const credentialsPath = path.join(configDir, 'credentials.yaml');
158
169
  // Check if credentials file exists
159
170
  if (!fs.existsSync(credentialsPath)) {
160
- this.error(`Credentials file not found at ${credentialsPath}\n` +
161
- `Create a profile using 'xano profile create'`);
171
+ this.error(`Credentials file not found at ${credentialsPath}\n` + `Create a profile using 'xano profile create'`);
162
172
  }
163
173
  // Read credentials file
164
174
  try {
@@ -73,8 +73,8 @@ Workspace: my-workspace (ID: 123)
73
73
  try {
74
74
  const response = await this.verboseFetch(apiUrl, {
75
75
  headers: {
76
- 'accept': 'application/json',
77
- 'Authorization': `Bearer ${profile.access_token}`,
76
+ accept: 'application/json',
77
+ Authorization: `Bearer ${profile.access_token}`,
78
78
  },
79
79
  method: 'GET',
80
80
  }, flags.verbose, profile.access_token);
@@ -82,7 +82,7 @@ Workspace: my-workspace (ID: 123)
82
82
  const errorText = await response.text();
83
83
  this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
84
84
  }
85
- const workspace = await response.json();
85
+ const workspace = (await response.json());
86
86
  // Output results
87
87
  if (flags.output === 'json') {
88
88
  this.log(JSON.stringify(workspace, null, 2));
@@ -94,13 +94,16 @@ Workspace: my-workspace (ID: 123)
94
94
  this.log(` Description: ${workspace.description}`);
95
95
  }
96
96
  if (workspace.created_at) {
97
- const createdDate = new Date(workspace.created_at * 1000).toISOString().split('T')[0];
97
+ const createdDate = new Date(workspace.created_at).toISOString().split('T')[0];
98
98
  this.log(` Created: ${createdDate}`);
99
99
  }
100
100
  if (workspace.updated_at) {
101
- const updatedDate = new Date(workspace.updated_at * 1000).toISOString().split('T')[0];
101
+ const updatedDate = new Date(workspace.updated_at).toISOString().split('T')[0];
102
102
  this.log(` Updated: ${updatedDate}`);
103
103
  }
104
+ if (workspace.preferences?.allow_push !== undefined) {
105
+ this.log(` Allow Push: ${workspace.preferences.allow_push}`);
106
+ }
104
107
  }
105
108
  }
106
109
  catch (error) {
@@ -117,8 +120,7 @@ Workspace: my-workspace (ID: 123)
117
120
  const credentialsPath = path.join(configDir, 'credentials.yaml');
118
121
  // Check if credentials file exists
119
122
  if (!fs.existsSync(credentialsPath)) {
120
- this.error(`Credentials file not found at ${credentialsPath}\n` +
121
- `Create a profile using 'xano profile create'`);
123
+ this.error(`Credentials file not found at ${credentialsPath}\n` + `Create a profile using 'xano profile create'`);
122
124
  }
123
125
  // Read credentials file
124
126
  try {
@@ -3,6 +3,7 @@ export default class WorkspaceList extends BaseCommand {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static flags: {
6
+ latest: import("@oclif/core/interfaces").BooleanFlag<boolean>;
6
7
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
8
  profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
9
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
@@ -45,6 +45,10 @@ Available workspaces:
45
45
  ];
46
46
  static flags = {
47
47
  ...BaseCommand.baseFlags,
48
+ latest: Flags.boolean({
49
+ default: false,
50
+ description: 'Sort by newest first (descending ID)',
51
+ }),
48
52
  output: Flags.string({
49
53
  char: 'o',
50
54
  default: 'summary',
@@ -78,8 +82,8 @@ Available workspaces:
78
82
  try {
79
83
  const response = await this.verboseFetch(apiUrl, {
80
84
  headers: {
81
- 'accept': 'application/json',
82
- 'Authorization': `Bearer ${profile.access_token}`,
85
+ accept: 'application/json',
86
+ Authorization: `Bearer ${profile.access_token}`,
83
87
  },
84
88
  method: 'GET',
85
89
  }, flags.verbose, profile.access_token);
@@ -87,7 +91,7 @@ Available workspaces:
87
91
  const errorText = await response.text();
88
92
  this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
89
93
  }
90
- const data = await response.json();
94
+ const data = (await response.json());
91
95
  // Handle different response formats
92
96
  let workspaces;
93
97
  if (Array.isArray(data)) {
@@ -99,6 +103,9 @@ Available workspaces:
99
103
  else {
100
104
  this.error('Unexpected API response format');
101
105
  }
106
+ if (flags.latest) {
107
+ workspaces.sort((a, b) => b.id - a.id);
108
+ }
102
109
  // Output results
103
110
  if (flags.output === 'json') {
104
111
  this.log(JSON.stringify(workspaces, null, 2));
@@ -111,11 +118,12 @@ Available workspaces:
111
118
  else {
112
119
  this.log('Available workspaces:');
113
120
  for (const workspace of workspaces) {
121
+ const created = workspace.created_at ? ` (created: ${workspace.created_at.split(' ')[0]})` : '';
114
122
  if (workspace.id === undefined) {
115
- this.log(` - ${workspace.name}`);
123
+ this.log(` - ${workspace.name}${created}`);
116
124
  }
117
125
  else {
118
- this.log(` - ${workspace.name} (ID: ${workspace.id})`);
126
+ this.log(` - ${workspace.name} (ID: ${workspace.id})${created}`);
119
127
  }
120
128
  }
121
129
  }
@@ -135,8 +143,7 @@ Available workspaces:
135
143
  const credentialsPath = path.join(configDir, 'credentials.yaml');
136
144
  // Check if credentials file exists
137
145
  if (!fs.existsSync(credentialsPath)) {
138
- this.error(`Credentials file not found at ${credentialsPath}\n` +
139
- `Create a profile using 'xano profile:create'`);
146
+ this.error(`Credentials file not found at ${credentialsPath}\n` + `Create a profile using 'xano profile:create'`);
140
147
  }
141
148
  // Read credentials file
142
149
  try {
@@ -282,6 +282,25 @@ Push functions but exclude test files
282
282
  }
283
283
  else {
284
284
  const errorText = await dryRunResponse.text();
285
+ // Check if push is disabled on this workspace
286
+ try {
287
+ const errorJson = JSON.parse(errorText);
288
+ if (errorJson.message?.includes('Push is disabled')) {
289
+ this.log('');
290
+ this.log(ux.colorize('red', ux.colorize('bold', 'Direct push to this workspace is disabled.')));
291
+ this.log(ux.colorize('dim', 'To apply changes to the workspace, use the sandbox review flow:'));
292
+ this.log(` ${ux.colorize('cyan', 'xano sandbox push')} ${ux.colorize('dim', '— push changes to your sandbox')}`);
293
+ this.log(` ${ux.colorize('cyan', 'xano sandbox review')} ${ux.colorize('dim', '— edit any logic, inspect the snapshot diff, and promote changes to the workspace')}`);
294
+ this.log('');
295
+ this.log(ux.colorize('dim', 'To enable direct push, go to Workspace Settings → CLI → Allow Direct Workspace Push.'));
296
+ this.log(ux.colorize('dim', 'Note: This setting does not apply to Free plan instances.'));
297
+ this.log('');
298
+ return;
299
+ }
300
+ }
301
+ catch {
302
+ // Not JSON, fall through
303
+ }
285
304
  this.warn(`Push preview failed (${dryRunResponse.status}). Skipping preview.`);
286
305
  if (flags.verbose) {
287
306
  this.log(ux.colorize('dim', errorText));
@@ -465,6 +484,15 @@ Push functions but exclude test files
465
484
  if (errorJson.payload?.param) {
466
485
  errorMessage += `\n Parameter: ${errorJson.payload.param}`;
467
486
  }
487
+ // Provide clear guidance when push is disabled
488
+ if (errorJson.message?.includes('Push is disabled')) {
489
+ this.error(`Push is disabled for this workspace.\n\n` +
490
+ `To enable, go to Workspace Settings and turn on "Allow Push".\n` +
491
+ `Note: This setting does not apply to Free plan instances.\n\n` +
492
+ `Alternatively, use sandbox commands:\n` +
493
+ ` xano sandbox push ${args.directory}\n` +
494
+ ` xano sandbox impersonate`);
495
+ }
468
496
  }
469
497
  catch {
470
498
  errorMessage += `\n${errorText}`;
@@ -635,7 +663,7 @@ Push functions but exclude test files
635
663
  const color = op.action === 'update' || op.action === 'update_field' ? 'yellow' : 'green';
636
664
  const actionLabel = op.action.toUpperCase();
637
665
  this.log(` ${ux.colorize(color, actionLabel.padEnd(16))} ${op.type.padEnd(18)} ${op.name}`);
638
- if (op.details) {
666
+ if (verbose && op.details) {
639
667
  this.log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', op.details)}`);
640
668
  }
641
669
  if (verbose && op.reason) {
@@ -656,7 +684,7 @@ Push functions but exclude test files
656
684
  const color = op.action === 'truncate' || op.action === 'alter_field' ? 'yellow' : 'red';
657
685
  const actionLabel = op.action.toUpperCase();
658
686
  this.log(` ${ux.colorize(color, actionLabel.padEnd(16))} ${op.type.padEnd(18)} ${op.name}`);
659
- if (op.details) {
687
+ if (verbose && op.details) {
660
688
  this.log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', op.details)}`);
661
689
  }
662
690
  if (verbose && op.reason) {
package/dist/help.d.ts CHANGED
@@ -12,9 +12,10 @@ declare class CustomCommandHelp extends BaseCommandHelp {
12
12
  protected flagHelpLabel(flag: Command.Flag.Any, showOptions?: boolean): string;
13
13
  }
14
14
  /**
15
- * Custom Help class that uses CustomCommandHelp
15
+ * Custom Help class that injects promoted commands into the COMMANDS list
16
16
  */
17
17
  export default class Help extends BaseHelp {
18
18
  protected CommandHelpClass: typeof CustomCommandHelp;
19
+ formatCommands(commands: Command.Loadable[]): string;
19
20
  }
20
21
  export {};
package/dist/help.js CHANGED
@@ -1,5 +1,15 @@
1
1
  import { Help as BaseHelp } from '@oclif/core';
2
2
  import { CommandHelp as BaseCommandHelp } from '@oclif/core/help';
3
+ /**
4
+ * Extra commands to include in the top-level COMMANDS list.
5
+ * These are nested commands promoted for discoverability.
6
+ */
7
+ const PROMOTED_COMMANDS = [
8
+ { description: 'Create a new workspace', label: 'workspace create' },
9
+ { description: 'List workspaces', label: 'workspace list' },
10
+ { description: 'Pull a workspace to local files', label: 'workspace pull' },
11
+ { description: 'Push local documents to a workspace', label: 'workspace push' },
12
+ ];
3
13
  /**
4
14
  * Custom CommandHelp class that extends the default to display environment variables
5
15
  * alongside flag descriptions
@@ -19,8 +29,36 @@ class CustomCommandHelp extends BaseCommandHelp {
19
29
  }
20
30
  }
21
31
  /**
22
- * Custom Help class that uses CustomCommandHelp
32
+ * Custom Help class that injects promoted commands into the COMMANDS list
23
33
  */
24
34
  export default class Help extends BaseHelp {
25
35
  CommandHelpClass = CustomCommandHelp;
36
+ formatCommands(commands) {
37
+ if (commands.length === 0 && PROMOTED_COMMANDS.length === 0)
38
+ return '';
39
+ // Check before IDs are mutated: root help has top-level commands (no colons)
40
+ const isRootHelp = commands.some((c) => !c.id.includes(':'));
41
+ const entries = commands
42
+ .filter((c) => (this.opts.hideAliasesFromRoot ? !c.aliases?.includes(c.id) : true))
43
+ .filter((c) => c.id !== 'plugins')
44
+ .map((c) => {
45
+ if (this.config.topicSeparator !== ':')
46
+ c.id = c.id.replaceAll(':', this.config.topicSeparator);
47
+ const summary = this.summary(c);
48
+ return [c.id, summary ? summary.replace(/\u001B\[\d+m/g, '') : ''];
49
+ });
50
+ // Only add promoted commands at the root level, not within a specific topic
51
+ if (isRootHelp) {
52
+ for (const promoted of PROMOTED_COMMANDS) {
53
+ entries.push([promoted.label, promoted.description]);
54
+ }
55
+ }
56
+ entries.sort((a, b) => a[0].localeCompare(b[0]));
57
+ const body = this.renderList(entries, {
58
+ indentation: 2,
59
+ spacer: '\n',
60
+ stripAnsi: this.opts.stripAnsi,
61
+ });
62
+ return this.section('COMMANDS', body + `\n\n\x1b[2mSee xano <topic> --help for all commands in a topic.\x1b[0m`);
63
+ }
26
64
  }