@xano/cli 0.0.95-beta.2 → 0.0.95-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 (100) hide show
  1. package/README.md +22 -12
  2. package/dist/base-command.d.ts +30 -0
  3. package/dist/base-command.js +61 -0
  4. package/dist/commands/auth/index.js +1 -1
  5. package/dist/commands/branch/create/index.d.ts +3 -1
  6. package/dist/commands/branch/create/index.js +21 -17
  7. package/dist/commands/profile/create/index.js +2 -2
  8. package/dist/commands/profile/edit/index.js +2 -2
  9. package/dist/commands/profile/me/index.js +21 -2
  10. package/dist/commands/profile/wizard/index.js +3 -3
  11. package/dist/commands/profile/workspace/set/index.js +1 -1
  12. package/dist/commands/{ephemeral → sandbox}/delete/index.d.ts +1 -5
  13. package/dist/commands/sandbox/delete/index.js +71 -0
  14. package/dist/commands/{ephemeral → sandbox}/env/delete/index.d.ts +1 -4
  15. package/dist/commands/{ephemeral → sandbox}/env/delete/index.js +20 -36
  16. package/dist/commands/{ephemeral → sandbox}/env/get/index.d.ts +1 -4
  17. package/dist/commands/sandbox/env/get/index.js +65 -0
  18. package/dist/commands/{ephemeral → sandbox}/env/get_all/index.d.ts +1 -4
  19. package/dist/commands/sandbox/env/get_all/index.js +78 -0
  20. package/dist/commands/{ephemeral → sandbox}/env/list/index.d.ts +1 -4
  21. package/dist/commands/sandbox/env/list/index.js +67 -0
  22. package/dist/commands/{ephemeral → sandbox}/env/set/index.d.ts +1 -4
  23. package/dist/commands/sandbox/env/set/index.js +74 -0
  24. package/dist/commands/{ephemeral → sandbox}/env/set_all/index.d.ts +1 -4
  25. package/dist/commands/{ephemeral → sandbox}/env/set_all/index.js +19 -35
  26. package/dist/commands/{ephemeral → sandbox}/get/index.d.ts +1 -4
  27. package/dist/commands/sandbox/get/index.js +63 -0
  28. package/dist/commands/sandbox/impersonate/index.d.ts +5 -0
  29. package/dist/commands/sandbox/impersonate/index.js +5 -0
  30. package/dist/commands/{ephemeral → sandbox}/license/get/index.d.ts +1 -4
  31. package/dist/commands/sandbox/license/get/index.js +78 -0
  32. package/dist/commands/{ephemeral → sandbox}/license/set/index.d.ts +1 -4
  33. package/dist/commands/{ephemeral → sandbox}/license/set/index.js +20 -36
  34. package/dist/commands/{ephemeral → sandbox}/pull/index.d.ts +1 -2
  35. package/dist/commands/{ephemeral → sandbox}/pull/index.js +13 -28
  36. package/dist/commands/{ephemeral → sandbox}/push/index.d.ts +3 -2
  37. package/dist/commands/{ephemeral → sandbox}/push/index.js +56 -31
  38. package/dist/commands/sandbox/reset/index.d.ts +12 -0
  39. package/dist/commands/sandbox/reset/index.js +71 -0
  40. package/dist/commands/{ephemeral/impersonate → sandbox/review}/index.d.ts +1 -4
  41. package/dist/commands/{ephemeral/impersonate → sandbox/review}/index.js +17 -33
  42. package/dist/commands/{ephemeral/unit_test/run_all → sandbox/unit_test/list}/index.d.ts +1 -2
  43. package/dist/commands/{ephemeral → sandbox}/unit_test/list/index.js +12 -26
  44. package/dist/commands/{ephemeral → sandbox}/unit_test/run/index.d.ts +1 -2
  45. package/dist/commands/{ephemeral → sandbox}/unit_test/run/index.js +11 -25
  46. package/dist/commands/{ephemeral/unit_test/list → sandbox/unit_test/run_all}/index.d.ts +1 -2
  47. package/dist/commands/{ephemeral → sandbox}/unit_test/run_all/index.js +11 -23
  48. package/dist/commands/{ephemeral/workflow_test/run_all → sandbox/workflow_test/list}/index.d.ts +1 -2
  49. package/dist/commands/{ephemeral → sandbox}/workflow_test/list/index.js +13 -27
  50. package/dist/commands/{ephemeral/workflow_test/get → sandbox/workflow_test/run}/index.d.ts +1 -2
  51. package/dist/commands/{ephemeral → sandbox}/workflow_test/run/index.js +11 -25
  52. package/dist/commands/{ephemeral/workflow_test/list → sandbox/workflow_test/run_all}/index.d.ts +1 -2
  53. package/dist/commands/{ephemeral → sandbox}/workflow_test/run_all/index.js +11 -23
  54. package/dist/commands/tenant/create/index.d.ts +2 -1
  55. package/dist/commands/tenant/create/index.js +23 -6
  56. package/dist/commands/tenant/deploy_release/index.d.ts +1 -0
  57. package/dist/commands/tenant/deploy_release/index.js +9 -1
  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.js +2 -27
  62. package/dist/commands/tenant/unit_test/run/index.js +2 -27
  63. package/dist/commands/tenant/unit_test/run_all/index.js +2 -27
  64. package/dist/commands/tenant/workflow_test/list/index.js +2 -27
  65. package/dist/commands/tenant/workflow_test/run/index.js +2 -27
  66. package/dist/commands/tenant/workflow_test/run_all/index.js +2 -27
  67. package/dist/commands/workspace/edit/index.d.ts +1 -0
  68. package/dist/commands/workspace/edit/index.js +16 -6
  69. package/dist/commands/workspace/get/index.js +9 -7
  70. package/dist/commands/workspace/list/index.d.ts +1 -0
  71. package/dist/commands/workspace/list/index.js +14 -7
  72. package/dist/commands/workspace/push/index.d.ts +2 -0
  73. package/dist/commands/workspace/push/index.js +81 -6
  74. package/dist/utils/reference-checker.d.ts +57 -0
  75. package/dist/utils/reference-checker.js +232 -0
  76. package/oclif.manifest.json +1745 -2451
  77. package/package.json +8 -8
  78. package/dist/commands/ephemeral/access/index.d.ts +0 -15
  79. package/dist/commands/ephemeral/access/index.js +0 -78
  80. package/dist/commands/ephemeral/create/index.d.ts +0 -17
  81. package/dist/commands/ephemeral/create/index.js +0 -102
  82. package/dist/commands/ephemeral/delete/index.js +0 -99
  83. package/dist/commands/ephemeral/env/get/index.js +0 -81
  84. package/dist/commands/ephemeral/env/get_all/index.js +0 -94
  85. package/dist/commands/ephemeral/env/list/index.js +0 -83
  86. package/dist/commands/ephemeral/env/set/index.js +0 -90
  87. package/dist/commands/ephemeral/get/index.js +0 -102
  88. package/dist/commands/ephemeral/license/get/index.js +0 -94
  89. package/dist/commands/ephemeral/list/index.d.ts +0 -15
  90. package/dist/commands/ephemeral/list/index.js +0 -109
  91. package/dist/commands/ephemeral/shared/index.d.ts +0 -15
  92. package/dist/commands/ephemeral/shared/index.js +0 -108
  93. package/dist/commands/ephemeral/workflow_test/delete/index.d.ts +0 -18
  94. package/dist/commands/ephemeral/workflow_test/delete/index.js +0 -75
  95. package/dist/commands/ephemeral/workflow_test/get/index.js +0 -77
  96. package/dist/commands/ephemeral/workflow_test/run/index.d.ts +0 -18
  97. package/dist/commands/tenant/workflow_test/delete/index.d.ts +0 -19
  98. package/dist/commands/tenant/workflow_test/delete/index.js +0 -110
  99. package/dist/commands/tenant/workflow_test/get/index.d.ts +0 -19
  100. package/dist/commands/tenant/workflow_test/get/index.js +0 -112
package/README.md CHANGED
@@ -145,6 +145,8 @@ xano workspace git pull ./output -r https://github.com/owner/repo --path subdir
145
145
 
146
146
  All branch commands use **branch labels** (e.g., `v1`, `dev`), not IDs.
147
147
 
148
+ The `v1` branch is the default branch and always exists. It cannot be created, edited, or deleted.
149
+
148
150
  ```bash
149
151
  # List branches
150
152
  xano branch list
@@ -153,9 +155,9 @@ xano branch list
153
155
  xano branch get <branch_label>
154
156
 
155
157
  # Create a branch
156
- xano branch create --label dev
157
- xano branch create -l feature-auth -s dev -d "Auth feature"
158
- xano branch create -l staging --color "#ebc346"
158
+ xano branch create dev
159
+ xano branch create feature-auth -s dev -d "Auth feature"
160
+ xano branch create staging --color "#ebc346"
159
161
 
160
162
  # Edit a branch
161
163
  xano branch edit <branch_label> --label "new-label"
@@ -307,7 +309,8 @@ xano tenant get <tenant_name>
307
309
 
308
310
  # Create a tenant
309
311
  xano tenant create "My Tenant"
310
- xano tenant create "My Tenant" -d "Description" --cluster_id 1 --platform_id 5
312
+ xano tenant create "My Tenant" -d "Description" --type tier2 --cluster_id 1 --platform_id 5
313
+ xano tenant create "My Tenant" --type tier2 --cluster_id 1 --license ./license.yaml
311
314
 
312
315
  # Edit a tenant
313
316
  xano tenant edit <tenant_name> --display "New Name" -d "New description"
@@ -445,18 +448,25 @@ xano tenant cluster license set <cluster_id>
445
448
  xano tenant cluster license set <cluster_id> --file ./kubeconfig.yaml
446
449
  ```
447
450
 
448
- ### Ephemeral
451
+ ### Sandbox
449
452
 
450
- Manage ephemeral tenants. Ephemeral tenants are workspace-agnostic (tier1 only) and do not run background tasks.
453
+ Manage your sandbox tenant. Each user has a single sandbox tenant that is auto-provisioned on first use.
451
454
 
452
455
  ```bash
453
- # Create an ephemeral tenant
454
- xano ephemeral create "My Ephemeral"
455
- xano ephemeral create "CI Tenant" -d "For CI/CD" -o json
456
+ # Get your sandbox tenant (creates if needed)
457
+ xano sandbox get
458
+ xano sandbox get -o json
459
+
460
+ # Push/pull workspace data
461
+ xano sandbox push ./my-workspace
462
+ xano sandbox pull ./my-tenant
463
+
464
+ # Impersonate (open in browser)
465
+ xano sandbox impersonate
456
466
 
457
- # Get ephemeral tenant details
458
- xano ephemeral get <tenant_name>
459
- xano ephemeral get <tenant_name> -o json
467
+ # Reset all workspace data
468
+ xano sandbox reset
469
+ xano sandbox reset --force
460
470
  ```
461
471
 
462
472
  ### Static Hosts
@@ -14,6 +14,17 @@ export interface CredentialsFile {
14
14
  };
15
15
  }
16
16
  export declare function buildUserAgent(version: string): string;
17
+ export interface SandboxTenant {
18
+ created_at?: string;
19
+ description?: string;
20
+ display?: string;
21
+ ephemeral?: boolean;
22
+ id: number;
23
+ name: string;
24
+ sandbox_expires_at?: string | number;
25
+ state?: string;
26
+ xano_domain?: string;
27
+ }
17
28
  export default abstract class BaseCommand extends Command {
18
29
  static baseFlags: {
19
30
  profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -38,6 +49,25 @@ export default abstract class BaseCommand extends Command {
38
49
  * Load and parse the credentials file. Returns null if the file doesn't exist.
39
50
  */
40
51
  protected loadCredentialsFile(): CredentialsFile | null;
52
+ /**
53
+ * Get or create the singleton sandbox environment for the authenticated user.
54
+ * Returns the sandbox object (existing or newly created).
55
+ */
56
+ protected getOrCreateSandbox(profile: ProfileConfig, verbose: boolean): Promise<SandboxTenant>;
57
+ /**
58
+ * Resolve profile from flags, validating instance_origin and access_token exist.
59
+ */
60
+ protected resolveProfile(flags: {
61
+ profile?: string;
62
+ }): {
63
+ profile: ProfileConfig;
64
+ profileName: string;
65
+ };
66
+ /**
67
+ * Parse an API error response and return a clean error message.
68
+ * Extracts the message from JSON responses and adds context for common errors.
69
+ */
70
+ protected parseApiError(response: Response, fallbackPrefix: string): Promise<string>;
41
71
  /**
42
72
  * Make an HTTP request with optional verbose logging.
43
73
  * Use this for all Metadata API calls to support the --verbose flag.
@@ -103,6 +103,67 @@ export default class BaseCommand extends Command {
103
103
  }
104
104
  return null;
105
105
  }
106
+ /**
107
+ * Get or create the singleton sandbox environment for the authenticated user.
108
+ * Returns the sandbox object (existing or newly created).
109
+ */
110
+ async getOrCreateSandbox(profile, verbose) {
111
+ const apiUrl = `${profile.instance_origin}/api:meta/sandbox/me`;
112
+ const response = await this.verboseFetch(apiUrl, {
113
+ headers: {
114
+ accept: 'application/json',
115
+ Authorization: `Bearer ${profile.access_token}`,
116
+ },
117
+ method: 'GET',
118
+ }, verbose, profile.access_token);
119
+ if (!response.ok) {
120
+ const message = await this.parseApiError(response, 'Failed to get sandbox environment');
121
+ this.error(message);
122
+ }
123
+ return (await response.json());
124
+ }
125
+ /**
126
+ * Resolve profile from flags, validating instance_origin and access_token exist.
127
+ */
128
+ resolveProfile(flags) {
129
+ const profileName = flags.profile || this.getDefaultProfile();
130
+ const credentials = this.loadCredentialsFile();
131
+ if (!credentials || !(profileName in credentials.profiles)) {
132
+ this.error(`Profile '${profileName}' not found.\n` + `Create a profile using 'xano profile create'`);
133
+ }
134
+ const profile = credentials.profiles[profileName];
135
+ if (!profile.instance_origin) {
136
+ this.error(`Profile '${profileName}' is missing instance_origin`);
137
+ }
138
+ if (!profile.access_token) {
139
+ this.error(`Profile '${profileName}' is missing access_token`);
140
+ }
141
+ return { profile, profileName };
142
+ }
143
+ /**
144
+ * Parse an API error response and return a clean error message.
145
+ * Extracts the message from JSON responses and adds context for common errors.
146
+ */
147
+ async parseApiError(response, fallbackPrefix) {
148
+ const errorText = await response.text();
149
+ let message = `${fallbackPrefix} (${response.status})`;
150
+ try {
151
+ const errorJson = JSON.parse(errorText);
152
+ if (errorJson.message) {
153
+ message = errorJson.message;
154
+ }
155
+ }
156
+ catch {
157
+ if (errorText) {
158
+ message += `\n${errorText}`;
159
+ }
160
+ }
161
+ // Provide guidance when sandbox access is denied (free plan restriction)
162
+ if (response.status === 500 && message === 'Access Denied.') {
163
+ message = 'Sandbox is not available on the Free plan. Upgrade your plan to use sandbox features.';
164
+ }
165
+ return message;
166
+ }
106
167
  /**
107
168
  * Make an HTTP request with optional verbose logging.
108
169
  * Use this for all Metadata API calls to support the --verbose flag.
@@ -283,7 +283,7 @@ Opening browser for Xano login at https://custom.xano.com...`,
283
283
  choices: [
284
284
  { name: '(Skip workspace)', value: '' },
285
285
  ...workspaces.map((ws) => ({
286
- name: ws.name,
286
+ name: `${ws.name} (${ws.id})`,
287
287
  value: ws.id,
288
288
  })),
289
289
  ],
@@ -1,11 +1,13 @@
1
1
  import BaseCommand from '../../../base-command.js';
2
2
  export default class BranchCreate extends BaseCommand {
3
3
  static description: string;
4
+ static args: {
5
+ label: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
6
+ };
4
7
  static examples: string[];
5
8
  static flags: {
6
9
  color: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
10
  description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
- label: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
11
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
12
  source: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
13
  workspace: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -1,4 +1,4 @@
1
- import { Flags } from '@oclif/core';
1
+ import { Args, Flags } from '@oclif/core';
2
2
  import * as yaml from 'js-yaml';
3
3
  import * as fs from 'node:fs';
4
4
  import * as os from 'node:os';
@@ -6,17 +6,23 @@ import * as path from 'node:path';
6
6
  import BaseCommand from '../../../base-command.js';
7
7
  export default class BranchCreate extends BaseCommand {
8
8
  static description = 'Create a new branch by cloning from an existing branch';
9
+ static args = {
10
+ label: Args.string({
11
+ description: 'Label for the new branch',
12
+ required: true,
13
+ }),
14
+ };
9
15
  static examples = [
10
- `$ xano branch create --label dev
16
+ `$ xano branch create dev
11
17
  Created branch: dev
12
18
  Cloned from: v1
13
19
  `,
14
- `$ xano branch create -l feature-auth -s dev -d "Authentication feature"
20
+ `$ xano branch create feature-auth -s dev -d "Authentication feature"
15
21
  Created branch: feature-auth
16
22
  Cloned from: dev
17
23
  Description: Authentication feature
18
24
  `,
19
- `$ xano branch create --label staging --color "#ebc346" --output json
25
+ `$ xano branch create staging --color "#ebc346" --output json
20
26
  {
21
27
  "created_at": "2024-02-11T10:00:00Z",
22
28
  "label": "staging",
@@ -37,11 +43,6 @@ Created branch: feature-auth
37
43
  description: 'Description for the new branch',
38
44
  required: false,
39
45
  }),
40
- label: Flags.string({
41
- char: 'l',
42
- description: 'Label for the new branch',
43
- required: true,
44
- }),
45
46
  output: Flags.string({
46
47
  char: 'o',
47
48
  default: 'summary',
@@ -62,7 +63,7 @@ Created branch: feature-auth
62
63
  }),
63
64
  };
64
65
  async run() {
65
- const { flags } = await this.parse(BranchCreate);
66
+ const { args, flags } = await this.parse(BranchCreate);
66
67
  // Get profile name (default or from flag/env)
67
68
  const profileName = flags.profile || this.getDefaultProfile();
68
69
  // Load credentials
@@ -84,11 +85,15 @@ Created branch: feature-auth
84
85
  const workspaceId = flags.workspace || profile.workspace;
85
86
  if (!workspaceId) {
86
87
  this.error('No workspace ID provided. Either use --workspace flag or set one in your profile.\n' +
87
- 'Usage: xano branch create --label <label> [--workspace <workspace_id>]');
88
+ 'Usage: xano branch create <label> [--workspace <workspace_id>]');
89
+ }
90
+ // Validate reserved branch names
91
+ if (args.label.toLowerCase() === 'v1') {
92
+ this.error("Cannot create a branch named 'v1'. This is the default branch and always exists.");
88
93
  }
89
94
  // Build request body
90
95
  const body = {
91
- label: flags.label,
96
+ label: args.label,
92
97
  source_branch: flags.source,
93
98
  };
94
99
  if (flags.description) {
@@ -104,8 +109,8 @@ Created branch: feature-auth
104
109
  const response = await this.verboseFetch(apiUrl, {
105
110
  body: JSON.stringify(body),
106
111
  headers: {
107
- 'accept': 'application/json',
108
- 'Authorization': `Bearer ${profile.access_token}`,
112
+ accept: 'application/json',
113
+ Authorization: `Bearer ${profile.access_token}`,
109
114
  'Content-Type': 'application/json',
110
115
  },
111
116
  method: 'POST',
@@ -114,7 +119,7 @@ Created branch: feature-auth
114
119
  const errorText = await response.text();
115
120
  this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
116
121
  }
117
- const branch = await response.json();
122
+ const branch = (await response.json());
118
123
  // Output results
119
124
  if (flags.output === 'json') {
120
125
  this.log(JSON.stringify(branch, null, 2));
@@ -145,8 +150,7 @@ Created branch: feature-auth
145
150
  const credentialsPath = path.join(configDir, 'credentials.yaml');
146
151
  // Check if credentials file exists
147
152
  if (!fs.existsSync(credentialsPath)) {
148
- this.error(`Credentials file not found at ${credentialsPath}\n` +
149
- `Create a profile using 'xano profile create'`);
153
+ this.error(`Credentials file not found at ${credentialsPath}\n` + `Create a profile using 'xano profile create'`);
150
154
  }
151
155
  // Read credentials file
152
156
  try {
@@ -98,8 +98,8 @@ Profile 'selfhosted' created successfully at ~/.xano/credentials.yaml
98
98
  const profileExists = args.name in credentials.profiles;
99
99
  credentials.profiles[args.name] = {
100
100
  access_token: flags.access_token,
101
- account_origin: flags.account_origin ?? '',
102
- instance_origin: flags.instance_origin,
101
+ account_origin: (flags.account_origin ?? '').replace(/\/+$/, ''),
102
+ instance_origin: flags.instance_origin.replace(/\/+$/, ''),
103
103
  ...(flags.workspace && { workspace: flags.workspace }),
104
104
  ...(flags.branch && { branch: flags.branch }),
105
105
  ...(flags.insecure && { insecure: true }),
@@ -128,8 +128,8 @@ Profile 'default' updated successfully at ~/.xano/credentials.yaml
128
128
  // Update only the fields that were provided
129
129
  const updatedProfile = {
130
130
  ...existingProfile,
131
- ...(flags.account_origin !== undefined && { account_origin: flags.account_origin }),
132
- ...(flags.instance_origin !== undefined && { instance_origin: flags.instance_origin }),
131
+ ...(flags.account_origin !== undefined && { account_origin: flags.account_origin.replace(/\/+$/, '') }),
132
+ ...(flags.instance_origin !== undefined && { instance_origin: flags.instance_origin.replace(/\/+$/, '') }),
133
133
  ...(flags.access_token !== undefined && { access_token: flags.access_token }),
134
134
  ...(flags.workspace !== undefined && { workspace: flags.workspace }),
135
135
  ...(flags.branch !== undefined && { branch: flags.branch }),
@@ -111,8 +111,27 @@ User Information:
111
111
  this.log(` Name: ${inst.name}`);
112
112
  if (inst.display)
113
113
  this.log(` Display: ${inst.display}`);
114
- if (profile.workspace)
115
- this.log(` Workspace: ${profile.workspace}`);
114
+ if (profile.workspace) {
115
+ let wsLabel = String(profile.workspace);
116
+ try {
117
+ const wsResponse = await this.verboseFetch(`${profile.instance_origin}/api:meta/workspace/${profile.workspace}`, {
118
+ headers: {
119
+ accept: 'application/json',
120
+ Authorization: `Bearer ${profile.access_token}`,
121
+ },
122
+ method: 'GET',
123
+ }, false, profile.access_token);
124
+ if (wsResponse.ok) {
125
+ const ws = (await wsResponse.json());
126
+ if (ws.name)
127
+ wsLabel = `${ws.name} (ID: ${profile.workspace})`;
128
+ }
129
+ }
130
+ catch {
131
+ // Fall back to just showing the ID
132
+ }
133
+ this.log(` Workspace: ${wsLabel}`);
134
+ }
116
135
  if (profile.branch)
117
136
  this.log(` Branch: ${profile.branch}`);
118
137
  }
@@ -126,7 +126,7 @@ Profile 'production' created successfully at ~/.xano/credentials.yaml
126
126
  choices: [
127
127
  { name: '(Skip workspace)', value: '' },
128
128
  ...workspaces.map((ws) => ({
129
- name: ws.name,
129
+ name: `${ws.name} (${ws.id})`,
130
130
  value: ws.id,
131
131
  })),
132
132
  ],
@@ -339,8 +339,8 @@ Profile 'production' created successfully at ~/.xano/credentials.yaml
339
339
  // Add or update the profile
340
340
  credentials.profiles[profile.name] = {
341
341
  access_token: profile.access_token,
342
- account_origin: profile.account_origin,
343
- instance_origin: profile.instance_origin,
342
+ account_origin: (profile.account_origin || '').replace(/\/+$/, ''),
343
+ instance_origin: profile.instance_origin.replace(/\/+$/, ''),
344
344
  ...(profile.workspace && { workspace: profile.workspace }),
345
345
  ...(profile.branch && { branch: profile.branch }),
346
346
  ...(profile.insecure && { insecure: true }),
@@ -40,7 +40,7 @@ Workspace updated to 'Production API' (xyz789) on profile 'production'
40
40
  const { selectedWorkspace } = await inquirer.prompt([
41
41
  {
42
42
  choices: workspaces.map((ws) => ({
43
- name: String(ws.id) === String(profile.workspace) ? `${ws.name} (current)` : ws.name,
43
+ name: String(ws.id) === String(profile.workspace) ? `${ws.name} (${ws.id}) (current)` : `${ws.name} (${ws.id})`,
44
44
  value: ws.id,
45
45
  })),
46
46
  message: 'Select a workspace',
@@ -1,13 +1,9 @@
1
1
  import BaseCommand from '../../../base-command.js';
2
- export default class EphemeralDelete extends BaseCommand {
3
- static args: {
4
- tenant_name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
- };
2
+ export default class SandboxDelete extends BaseCommand {
6
3
  static description: string;
7
4
  static examples: string[];
8
5
  static flags: {
9
6
  force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
- output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
7
  profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
8
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
9
  };
@@ -0,0 +1,71 @@
1
+ import { Flags } from '@oclif/core';
2
+ import BaseCommand from '../../../base-command.js';
3
+ export default class SandboxDelete extends BaseCommand {
4
+ static description = 'Delete your sandbox environment completely (debugging only — it will be re-created on next access)';
5
+ static examples = [
6
+ `$ xano sandbox delete
7
+ Are you sure you want to DELETE your sandbox environment? This destroys all data. (y/N) y
8
+ Sandbox environment deleted.
9
+ `,
10
+ `$ xano sandbox delete --force`,
11
+ ];
12
+ static flags = {
13
+ ...BaseCommand.baseFlags,
14
+ force: Flags.boolean({
15
+ char: 'f',
16
+ default: false,
17
+ description: 'Skip confirmation prompt',
18
+ required: false,
19
+ }),
20
+ };
21
+ async run() {
22
+ const { flags } = await this.parse(SandboxDelete);
23
+ const { profile } = this.resolveProfile(flags);
24
+ if (!flags.force) {
25
+ const confirmed = await this.confirm(`Are you sure you want to DELETE your sandbox environment? This destroys all data and the tenant will be re-created on next access.`);
26
+ if (!confirmed) {
27
+ this.log('Delete cancelled.');
28
+ return;
29
+ }
30
+ }
31
+ const apiUrl = `${profile.instance_origin}/api:meta/sandbox/me`;
32
+ try {
33
+ const response = await this.verboseFetch(apiUrl, {
34
+ headers: {
35
+ accept: 'application/json',
36
+ Authorization: `Bearer ${profile.access_token}`,
37
+ 'Content-Type': 'application/json',
38
+ },
39
+ method: 'DELETE',
40
+ }, flags.verbose, profile.access_token);
41
+ if (!response.ok) {
42
+ const message = await this.parseApiError(response, 'Failed to delete sandbox environment');
43
+ this.error(message);
44
+ }
45
+ this.log('Sandbox environment deleted.');
46
+ }
47
+ catch (error) {
48
+ if (error instanceof Error && 'oclif' in error)
49
+ throw error;
50
+ if (error instanceof Error) {
51
+ this.error(`Failed to delete sandbox environment: ${error.message}`);
52
+ }
53
+ else {
54
+ this.error(`Failed to delete sandbox environment: ${String(error)}`);
55
+ }
56
+ }
57
+ }
58
+ async confirm(message) {
59
+ const readline = await import('node:readline');
60
+ const rl = readline.createInterface({
61
+ input: process.stdin,
62
+ output: process.stdout,
63
+ });
64
+ return new Promise((resolve) => {
65
+ rl.question(`${message} (y/N) `, (answer) => {
66
+ rl.close();
67
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
68
+ });
69
+ });
70
+ }
71
+ }
@@ -1,8 +1,5 @@
1
1
  import BaseCommand from '../../../../base-command.js';
2
- export default class EphemeralEnvDelete extends BaseCommand {
3
- static args: {
4
- tenant_name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
- };
2
+ export default class SandboxEnvDelete extends BaseCommand {
6
3
  static description: string;
7
4
  static examples: string[];
8
5
  static flags: {
@@ -1,20 +1,14 @@
1
- import { Args, Flags } from '@oclif/core';
1
+ import { Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../../base-command.js';
3
- export default class EphemeralEnvDelete extends BaseCommand {
4
- static args = {
5
- tenant_name: Args.string({
6
- description: 'Ephemeral tenant name',
7
- required: true,
8
- }),
9
- };
10
- static description = 'Delete an environment variable from an ephemeral tenant';
3
+ export default class SandboxEnvDelete extends BaseCommand {
4
+ static description = 'Delete an environment variable from a sandbox environment';
11
5
  static examples = [
12
- `$ xano ephemeral env delete my-tenant --name DATABASE_URL
13
- Are you sure you want to delete environment variable 'DATABASE_URL' from ephemeral tenant my-tenant? (y/N) y
14
- Environment variable 'DATABASE_URL' deleted from ephemeral tenant my-tenant
6
+ `$ xano sandbox env delete --name DATABASE_URL
7
+ Are you sure you want to delete environment variable 'DATABASE_URL'? (y/N) y
8
+ Environment variable 'DATABASE_URL' deleted
15
9
  `,
16
- `$ xano ephemeral env delete my-tenant --name DATABASE_URL --force`,
17
- `$ xano ephemeral env delete my-tenant --name DATABASE_URL -f -o json`,
10
+ `$ xano sandbox env delete --name DATABASE_URL --force`,
11
+ `$ xano sandbox env delete --name DATABASE_URL -f -o json`,
18
12
  ];
19
13
  static flags = {
20
14
  ...BaseCommand.baseFlags,
@@ -38,29 +32,17 @@ Environment variable 'DATABASE_URL' deleted from ephemeral tenant my-tenant
38
32
  }),
39
33
  };
40
34
  async run() {
41
- const { args, flags } = await this.parse(EphemeralEnvDelete);
42
- const profileName = flags.profile || this.getDefaultProfile();
43
- const credentials = this.loadCredentialsFile();
44
- if (!credentials || !(profileName in credentials.profiles)) {
45
- this.error(`Profile '${profileName}' not found.\n` + `Create a profile using 'xano profile create'`);
46
- }
47
- const profile = credentials.profiles[profileName];
48
- if (!profile.instance_origin) {
49
- this.error(`Profile '${profileName}' is missing instance_origin`);
50
- }
51
- if (!profile.access_token) {
52
- this.error(`Profile '${profileName}' is missing access_token`);
53
- }
54
- const tenantName = args.tenant_name;
35
+ const { flags } = await this.parse(SandboxEnvDelete);
36
+ const { profile } = this.resolveProfile(flags);
55
37
  const envName = flags.name;
56
38
  if (!flags.force) {
57
- const confirmed = await this.confirm(`Are you sure you want to delete environment variable '${envName}' from ephemeral tenant ${tenantName}?`);
39
+ const confirmed = await this.confirm(`Are you sure you want to delete environment variable '${envName}' from sandbox environment?`);
58
40
  if (!confirmed) {
59
41
  this.log('Deletion cancelled.');
60
42
  return;
61
43
  }
62
44
  }
63
- const apiUrl = `${profile.instance_origin}/api:meta/ephemeral/tenant/${tenantName}/env/${envName}`;
45
+ const apiUrl = `${profile.instance_origin}/api:meta/sandbox/env/${envName}`;
64
46
  try {
65
47
  const response = await this.verboseFetch(apiUrl, {
66
48
  headers: {
@@ -70,22 +52,24 @@ Environment variable 'DATABASE_URL' deleted from ephemeral tenant my-tenant
70
52
  method: 'DELETE',
71
53
  }, flags.verbose, profile.access_token);
72
54
  if (!response.ok) {
73
- const errorText = await response.text();
74
- this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
55
+ const message = await this.parseApiError(response, 'API request failed');
56
+ this.error(message);
75
57
  }
76
58
  if (flags.output === 'json') {
77
- this.log(JSON.stringify({ deleted: true, env_name: envName, tenant_name: tenantName }, null, 2));
59
+ this.log(JSON.stringify({ deleted: true, env_name: envName }, null, 2));
78
60
  }
79
61
  else {
80
- this.log(`Environment variable '${envName}' deleted from ephemeral tenant ${tenantName}`);
62
+ this.log(`Environment variable '${envName}' deleted from sandbox environment`);
81
63
  }
82
64
  }
83
65
  catch (error) {
66
+ if (error instanceof Error && 'oclif' in error)
67
+ throw error;
84
68
  if (error instanceof Error) {
85
- this.error(`Failed to delete ephemeral tenant environment variable: ${error.message}`);
69
+ this.error(`Failed to delete sandbox environment variable: ${error.message}`);
86
70
  }
87
71
  else {
88
- this.error(`Failed to delete ephemeral tenant environment variable: ${String(error)}`);
72
+ this.error(`Failed to delete sandbox environment variable: ${String(error)}`);
89
73
  }
90
74
  }
91
75
  }
@@ -1,8 +1,5 @@
1
1
  import BaseCommand from '../../../../base-command.js';
2
- export default class EphemeralEnvGet extends BaseCommand {
3
- static args: {
4
- tenant_name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
- };
2
+ export default class SandboxEnvGet extends BaseCommand {
6
3
  static description: string;
7
4
  static examples: string[];
8
5
  static flags: {