@xano/cli 0.0.26 → 0.0.28

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 (110) hide show
  1. package/dist/base-command.d.ts +3 -1
  2. package/dist/base-command.js +12 -5
  3. package/dist/commands/auth/index.d.ts +21 -0
  4. package/dist/commands/auth/index.js +533 -0
  5. package/dist/commands/branch/create/index.d.ts +17 -0
  6. package/dist/commands/branch/create/index.js +164 -0
  7. package/dist/commands/branch/delete/index.d.ts +18 -0
  8. package/dist/commands/branch/delete/index.js +156 -0
  9. package/dist/commands/branch/edit/index.d.ts +19 -0
  10. package/dist/commands/branch/edit/index.js +166 -0
  11. package/dist/commands/branch/get/index.d.ts +16 -0
  12. package/dist/commands/branch/get/index.js +135 -0
  13. package/dist/commands/branch/list/index.d.ts +18 -0
  14. package/dist/commands/branch/list/index.js +138 -0
  15. package/dist/commands/branch/set-live/index.d.ts +18 -0
  16. package/dist/commands/branch/set-live/index.js +155 -0
  17. package/dist/commands/function/create/index.d.ts +7 -6
  18. package/dist/commands/function/create/index.js +55 -55
  19. package/dist/commands/function/edit/index.d.ts +11 -10
  20. package/dist/commands/function/edit/index.js +155 -162
  21. package/dist/commands/function/get/index.d.ts +6 -5
  22. package/dist/commands/function/get/index.js +55 -60
  23. package/dist/commands/function/list/index.d.ts +6 -5
  24. package/dist/commands/function/list/index.js +52 -52
  25. package/dist/commands/profile/create/index.d.ts +6 -6
  26. package/dist/commands/profile/create/index.js +37 -37
  27. package/dist/commands/profile/delete/index.d.ts +2 -2
  28. package/dist/commands/profile/delete/index.js +9 -9
  29. package/dist/commands/profile/edit/index.d.ts +8 -7
  30. package/dist/commands/profile/edit/index.js +48 -48
  31. package/dist/commands/profile/get-default/index.js +1 -1
  32. package/dist/commands/profile/list/index.d.ts +2 -2
  33. package/dist/commands/profile/list/index.js +9 -9
  34. package/dist/commands/profile/me/index.d.ts +4 -3
  35. package/dist/commands/profile/me/index.js +21 -21
  36. package/dist/commands/profile/project/index.js +1 -1
  37. package/dist/commands/profile/set-default/index.js +1 -1
  38. package/dist/commands/profile/token/index.js +1 -1
  39. package/dist/commands/profile/wizard/index.d.ts +5 -4
  40. package/dist/commands/profile/wizard/index.js +142 -108
  41. package/dist/commands/run/env/delete/index.d.ts +3 -2
  42. package/dist/commands/run/env/delete/index.js +10 -10
  43. package/dist/commands/run/env/get/index.d.ts +3 -2
  44. package/dist/commands/run/env/get/index.js +11 -11
  45. package/dist/commands/run/env/list/index.d.ts +3 -2
  46. package/dist/commands/run/env/list/index.js +17 -19
  47. package/dist/commands/run/env/set/index.d.ts +3 -2
  48. package/dist/commands/run/env/set/index.js +5 -5
  49. package/dist/commands/run/exec/index.d.ts +19 -8
  50. package/dist/commands/run/exec/index.js +186 -108
  51. package/dist/commands/run/info/index.d.ts +5 -4
  52. package/dist/commands/run/info/index.js +27 -27
  53. package/dist/commands/run/projects/create/index.d.ts +4 -3
  54. package/dist/commands/run/projects/create/index.js +23 -23
  55. package/dist/commands/run/projects/delete/index.d.ts +3 -2
  56. package/dist/commands/run/projects/delete/index.js +10 -10
  57. package/dist/commands/run/projects/list/index.d.ts +3 -2
  58. package/dist/commands/run/projects/list/index.js +12 -12
  59. package/dist/commands/run/projects/update/index.d.ts +4 -3
  60. package/dist/commands/run/projects/update/index.js +21 -21
  61. package/dist/commands/run/secrets/delete/index.d.ts +3 -2
  62. package/dist/commands/run/secrets/delete/index.js +10 -10
  63. package/dist/commands/run/secrets/get/index.d.ts +3 -2
  64. package/dist/commands/run/secrets/get/index.js +11 -11
  65. package/dist/commands/run/secrets/list/index.d.ts +3 -2
  66. package/dist/commands/run/secrets/list/index.js +22 -24
  67. package/dist/commands/run/secrets/set/index.d.ts +4 -3
  68. package/dist/commands/run/secrets/set/index.js +16 -16
  69. package/dist/commands/run/sessions/delete/index.d.ts +3 -2
  70. package/dist/commands/run/sessions/delete/index.js +10 -10
  71. package/dist/commands/run/sessions/get/index.d.ts +3 -2
  72. package/dist/commands/run/sessions/get/index.js +11 -11
  73. package/dist/commands/run/sessions/list/index.d.ts +3 -2
  74. package/dist/commands/run/sessions/list/index.js +11 -11
  75. package/dist/commands/run/sessions/start/index.d.ts +3 -2
  76. package/dist/commands/run/sessions/start/index.js +11 -11
  77. package/dist/commands/run/sessions/stop/index.d.ts +3 -2
  78. package/dist/commands/run/sessions/stop/index.js +11 -11
  79. package/dist/commands/run/sink/get/index.d.ts +3 -2
  80. package/dist/commands/run/sink/get/index.js +11 -11
  81. package/dist/commands/static_host/build/create/index.d.ts +5 -4
  82. package/dist/commands/static_host/build/create/index.js +33 -33
  83. package/dist/commands/static_host/build/get/index.d.ts +5 -4
  84. package/dist/commands/static_host/build/get/index.js +20 -20
  85. package/dist/commands/static_host/build/list/index.d.ts +4 -3
  86. package/dist/commands/static_host/build/list/index.js +31 -31
  87. package/dist/commands/static_host/list/index.d.ts +4 -3
  88. package/dist/commands/static_host/list/index.js +31 -31
  89. package/dist/commands/workspace/create/index.d.ts +14 -0
  90. package/dist/commands/workspace/create/index.js +131 -0
  91. package/dist/commands/workspace/delete/index.d.ts +20 -0
  92. package/dist/commands/workspace/delete/index.js +141 -0
  93. package/dist/commands/workspace/edit/index.d.ts +22 -0
  94. package/dist/commands/workspace/edit/index.js +176 -0
  95. package/dist/commands/workspace/get/index.d.ts +18 -0
  96. package/dist/commands/workspace/get/index.js +136 -0
  97. package/dist/commands/workspace/list/index.d.ts +3 -2
  98. package/dist/commands/workspace/list/index.js +15 -15
  99. package/dist/commands/workspace/pull/index.d.ts +6 -4
  100. package/dist/commands/workspace/pull/index.js +123 -63
  101. package/dist/commands/workspace/push/index.d.ts +2 -0
  102. package/dist/commands/workspace/push/index.js +16 -5
  103. package/dist/help.d.ts +1 -1
  104. package/dist/lib/base-run-command.d.ts +6 -6
  105. package/dist/lib/base-run-command.js +8 -6
  106. package/dist/lib/run-http-client.d.ts +24 -18
  107. package/dist/lib/run-http-client.js +96 -61
  108. package/dist/lib/run-types.d.ts +80 -80
  109. package/oclif.manifest.json +2041 -843
  110. package/package.json +1 -1
@@ -0,0 +1,136 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import * as yaml from 'js-yaml';
3
+ import * as fs from 'node:fs';
4
+ import * as os from 'node:os';
5
+ import * as path from 'node:path';
6
+ import BaseCommand from '../../../base-command.js';
7
+ export default class WorkspaceGet extends BaseCommand {
8
+ static args = {
9
+ workspace_id: Args.integer({
10
+ description: 'Workspace ID to get details for (uses profile workspace if not provided)',
11
+ required: false,
12
+ }),
13
+ };
14
+ static description = 'Get details of a specific workspace from the Xano Metadata API';
15
+ static examples = [
16
+ `$ xano workspace get 123
17
+ Workspace: my-workspace (ID: 123)
18
+ Description: My workspace description
19
+ Created: 2024-01-15
20
+ `,
21
+ `$ xano workspace get --output json
22
+ {
23
+ "id": 123,
24
+ "name": "my-workspace",
25
+ "description": "My workspace description"
26
+ }
27
+ `,
28
+ `$ xano workspace get 456 -p production -o json
29
+ {
30
+ "id": 456,
31
+ "name": "production-workspace"
32
+ }
33
+ `,
34
+ ];
35
+ static flags = {
36
+ ...BaseCommand.baseFlags,
37
+ output: Flags.string({
38
+ char: 'o',
39
+ default: 'summary',
40
+ description: 'Output format',
41
+ options: ['summary', 'json'],
42
+ required: false,
43
+ }),
44
+ };
45
+ async run() {
46
+ const { args, flags } = await this.parse(WorkspaceGet);
47
+ // Get profile name (default or from flag/env)
48
+ const profileName = flags.profile || this.getDefaultProfile();
49
+ // Load credentials
50
+ const credentials = this.loadCredentials();
51
+ // Get the profile configuration
52
+ if (!(profileName in credentials.profiles)) {
53
+ this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
54
+ `Create a profile using 'xano profile create'`);
55
+ }
56
+ const profile = credentials.profiles[profileName];
57
+ // Validate required fields
58
+ if (!profile.instance_origin) {
59
+ this.error(`Profile '${profileName}' is missing instance_origin`);
60
+ }
61
+ if (!profile.access_token) {
62
+ this.error(`Profile '${profileName}' is missing access_token`);
63
+ }
64
+ // Get workspace ID from args or profile
65
+ const workspaceId = args.workspace_id || profile.workspace;
66
+ if (!workspaceId) {
67
+ this.error('No workspace ID provided. Either pass a workspace ID as an argument or set one in your profile.\n' +
68
+ 'Usage: xano workspace get <workspace_id>');
69
+ }
70
+ // Construct the API URL
71
+ const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}`;
72
+ // Fetch workspace from the API
73
+ try {
74
+ const response = await fetch(apiUrl, {
75
+ headers: {
76
+ 'accept': 'application/json',
77
+ 'Authorization': `Bearer ${profile.access_token}`,
78
+ },
79
+ method: 'GET',
80
+ });
81
+ if (!response.ok) {
82
+ const errorText = await response.text();
83
+ this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
84
+ }
85
+ const workspace = await response.json();
86
+ // Output results
87
+ if (flags.output === 'json') {
88
+ this.log(JSON.stringify(workspace, null, 2));
89
+ }
90
+ else {
91
+ // summary format
92
+ this.log(`Workspace: ${workspace.name} (ID: ${workspace.id})`);
93
+ if (workspace.description) {
94
+ this.log(` Description: ${workspace.description}`);
95
+ }
96
+ if (workspace.created_at) {
97
+ const createdDate = new Date(workspace.created_at * 1000).toISOString().split('T')[0];
98
+ this.log(` Created: ${createdDate}`);
99
+ }
100
+ if (workspace.updated_at) {
101
+ const updatedDate = new Date(workspace.updated_at * 1000).toISOString().split('T')[0];
102
+ this.log(` Updated: ${updatedDate}`);
103
+ }
104
+ }
105
+ }
106
+ catch (error) {
107
+ if (error instanceof Error) {
108
+ this.error(`Failed to fetch workspace: ${error.message}`);
109
+ }
110
+ else {
111
+ this.error(`Failed to fetch workspace: ${String(error)}`);
112
+ }
113
+ }
114
+ }
115
+ loadCredentials() {
116
+ const configDir = path.join(os.homedir(), '.xano');
117
+ const credentialsPath = path.join(configDir, 'credentials.yaml');
118
+ // Check if credentials file exists
119
+ if (!fs.existsSync(credentialsPath)) {
120
+ this.error(`Credentials file not found at ${credentialsPath}\n` +
121
+ `Create a profile using 'xano profile create'`);
122
+ }
123
+ // Read credentials file
124
+ try {
125
+ const fileContent = fs.readFileSync(credentialsPath, 'utf8');
126
+ const parsed = yaml.load(fileContent);
127
+ if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
128
+ this.error('Credentials file has invalid format.');
129
+ }
130
+ return parsed;
131
+ }
132
+ catch (error) {
133
+ this.error(`Failed to parse credentials file: ${error}`);
134
+ }
135
+ }
136
+ }
@@ -1,11 +1,12 @@
1
1
  import BaseCommand from '../../../base-command.js';
2
2
  export default class WorkspaceList extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
3
5
  static flags: {
4
6
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
5
7
  profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
6
9
  };
7
- static description: string;
8
- static examples: string[];
9
10
  run(): Promise<void>;
10
11
  private loadCredentials;
11
12
  }
@@ -1,20 +1,10 @@
1
1
  import { Flags } from '@oclif/core';
2
+ import * as yaml from 'js-yaml';
2
3
  import * as fs from 'node:fs';
3
4
  import * as os from 'node:os';
4
5
  import * as path from 'node:path';
5
- import * as yaml from 'js-yaml';
6
6
  import BaseCommand from '../../../base-command.js';
7
7
  export default class WorkspaceList extends BaseCommand {
8
- static flags = {
9
- ...BaseCommand.baseFlags,
10
- output: Flags.string({
11
- char: 'o',
12
- description: 'Output format',
13
- required: false,
14
- default: 'summary',
15
- options: ['summary', 'json'],
16
- }),
17
- };
18
8
  static description = 'List all workspaces from the Xano Metadata API';
19
9
  static examples = [
20
10
  `$ xano workspace:list
@@ -53,6 +43,16 @@ Available workspaces:
53
43
  }
54
44
  `,
55
45
  ];
46
+ static flags = {
47
+ ...BaseCommand.baseFlags,
48
+ output: Flags.string({
49
+ char: 'o',
50
+ default: 'summary',
51
+ description: 'Output format',
52
+ options: ['summary', 'json'],
53
+ required: false,
54
+ }),
55
+ };
56
56
  async run() {
57
57
  const { flags } = await this.parse(WorkspaceList);
58
58
  // Get profile name (default or from flag/env)
@@ -77,11 +77,11 @@ Available workspaces:
77
77
  // Fetch workspaces from the API
78
78
  try {
79
79
  const response = await fetch(apiUrl, {
80
- method: 'GET',
81
80
  headers: {
82
81
  'accept': 'application/json',
83
82
  'Authorization': `Bearer ${profile.access_token}`,
84
83
  },
84
+ method: 'GET',
85
85
  });
86
86
  if (!response.ok) {
87
87
  const errorText = await response.text();
@@ -111,11 +111,11 @@ Available workspaces:
111
111
  else {
112
112
  this.log('Available workspaces:');
113
113
  for (const workspace of workspaces) {
114
- if (workspace.id !== undefined) {
115
- this.log(` - ${workspace.name} (ID: ${workspace.id})`);
114
+ if (workspace.id === undefined) {
115
+ this.log(` - ${workspace.name}`);
116
116
  }
117
117
  else {
118
- this.log(` - ${workspace.name}`);
118
+ this.log(` - ${workspace.name} (ID: ${workspace.id})`);
119
119
  }
120
120
  }
121
121
  }
@@ -3,15 +3,18 @@ export default class Pull extends BaseCommand {
3
3
  static args: {
4
4
  directory: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
5
  };
6
+ static description: string;
7
+ static examples: string[];
6
8
  static flags: {
7
- workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
10
  env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
11
  records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
13
  profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
15
  };
12
- static description: string;
13
- static examples: string[];
14
16
  run(): Promise<void>;
17
+ private loadCredentials;
15
18
  /**
16
19
  * Parse a single document to extract its type, name, and optional verb.
17
20
  * Skips leading comment lines (starting with //) to find the first
@@ -24,5 +27,4 @@ export default class Pull extends BaseCommand {
24
27
  * characters that are unsafe in filenames.
25
28
  */
26
29
  private sanitizeFilename;
27
- private loadCredentials;
28
30
  }
@@ -1,8 +1,9 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
+ import * as yaml from 'js-yaml';
2
3
  import * as fs from 'node:fs';
3
4
  import * as os from 'node:os';
4
5
  import * as path from 'node:path';
5
- import * as yaml from 'js-yaml';
6
+ import snakeCase from 'lodash.snakecase';
6
7
  import BaseCommand from '../../../base-command.js';
7
8
  export default class Pull extends BaseCommand {
8
9
  static args = {
@@ -11,36 +12,44 @@ export default class Pull extends BaseCommand {
11
12
  required: true,
12
13
  }),
13
14
  };
15
+ static description = 'Pull a workspace multidoc from the Xano Metadata API and split into individual files';
16
+ static examples = [
17
+ `$ xano workspace pull ./my-workspace
18
+ Pulled 42 documents to ./my-workspace
19
+ `,
20
+ `$ xano workspace pull ./output -w 40
21
+ Pulled 15 documents to ./output
22
+ `,
23
+ `$ xano workspace pull ./backup --profile production --env --records
24
+ Pulled 58 documents to ./backup
25
+ `,
26
+ `$ xano workspace pull ./my-workspace -b dev
27
+ Pulled 42 documents to ./my-workspace
28
+ `,
29
+ ];
14
30
  static flags = {
15
31
  ...BaseCommand.baseFlags,
16
- workspace: Flags.string({
17
- char: 'w',
18
- description: 'Workspace ID (optional if set in profile)',
32
+ branch: Flags.string({
33
+ char: 'b',
34
+ description: 'Branch name (optional if set in profile, defaults to live)',
19
35
  required: false,
20
36
  }),
21
37
  env: Flags.boolean({
38
+ default: false,
22
39
  description: 'Include environment variables',
23
40
  required: false,
24
- default: false,
25
41
  }),
26
42
  records: Flags.boolean({
43
+ default: false,
27
44
  description: 'Include records',
28
45
  required: false,
29
- default: false,
46
+ }),
47
+ workspace: Flags.string({
48
+ char: 'w',
49
+ description: 'Workspace ID (optional if set in profile)',
50
+ required: false,
30
51
  }),
31
52
  };
32
- static description = 'Pull a workspace multidoc from the Xano Metadata API and split into individual files';
33
- static examples = [
34
- `$ xano workspace pull ./my-workspace
35
- Pulled 42 documents to ./my-workspace
36
- `,
37
- `$ xano workspace pull ./output -w 40
38
- Pulled 15 documents to ./output
39
- `,
40
- `$ xano workspace pull ./backup --profile production --env --records
41
- Pulled 58 documents to ./backup
42
- `,
43
- ];
44
53
  async run() {
45
54
  const { args, flags } = await this.parse(Pull);
46
55
  // Get profile name (default or from flag/env)
@@ -73,22 +82,25 @@ Pulled 58 documents to ./backup
73
82
  ` 1. Provide it as a flag: xano workspace pull <directory> -w <workspace_id>\n` +
74
83
  ` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
75
84
  }
85
+ // Determine branch from flag or profile
86
+ const branch = flags.branch || profile.branch || '';
76
87
  // Build query parameters
77
88
  const queryParams = new URLSearchParams({
89
+ branch,
78
90
  env: flags.env.toString(),
79
91
  records: flags.records.toString(),
80
92
  });
81
93
  // Construct the API URL
82
- const apiUrl = `${profile.instance_origin}/api:meta/beta/workspace/${workspaceId}/multidoc?${queryParams.toString()}`;
94
+ const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/multidoc?${queryParams.toString()}`;
83
95
  // Fetch multidoc from the API
84
96
  let responseText;
85
97
  try {
86
98
  const response = await fetch(apiUrl, {
87
- method: 'GET',
88
99
  headers: {
89
100
  'accept': 'application/json',
90
101
  'Authorization': `Bearer ${profile.access_token}`,
91
102
  },
103
+ method: 'GET',
92
104
  });
93
105
  if (!response.ok) {
94
106
  const errorText = await response.text();
@@ -130,35 +142,101 @@ Pulled 58 documents to ./backup
130
142
  const filenameCounters = new Map();
131
143
  let writtenCount = 0;
132
144
  for (const doc of documents) {
133
- // Create the type subdirectory
134
- const typeDir = path.join(outputDir, doc.type);
135
- fs.mkdirSync(typeDir, { recursive: true });
136
- // Build the base filename
137
- let baseName = this.sanitizeFilename(doc.name);
138
- if (doc.verb) {
139
- baseName = `${baseName}_${doc.verb}`;
145
+ let typeDir;
146
+ let baseName;
147
+ if (doc.type === 'workspace') {
148
+ // workspace workspace/{name}.xs
149
+ typeDir = path.join(outputDir, 'workspace');
150
+ baseName = this.sanitizeFilename(doc.name);
151
+ }
152
+ else if (doc.type === 'workspace_trigger') {
153
+ // workspace_trigger → workspace/trigger/{name}.xs
154
+ typeDir = path.join(outputDir, 'workspace', 'trigger');
155
+ baseName = this.sanitizeFilename(doc.name);
156
+ }
157
+ else if (doc.type === 'agent_trigger') {
158
+ // agent_trigger → agent/trigger/{name}.xs
159
+ typeDir = path.join(outputDir, 'agent', 'trigger');
160
+ baseName = this.sanitizeFilename(doc.name);
161
+ }
162
+ else if (doc.type === 'mcp_server_trigger') {
163
+ // mcp_server_trigger → mcp_server/trigger/{name}.xs
164
+ typeDir = path.join(outputDir, 'mcp_server', 'trigger');
165
+ baseName = this.sanitizeFilename(doc.name);
166
+ }
167
+ else if (doc.type === 'table_trigger') {
168
+ // table_trigger → table/trigger/{name}.xs
169
+ typeDir = path.join(outputDir, 'table', 'trigger');
170
+ baseName = this.sanitizeFilename(doc.name);
171
+ }
172
+ else if (doc.type === 'api_group') {
173
+ // api_group "test" → api/test/api_group.xs
174
+ const groupFolder = snakeCase(doc.name);
175
+ typeDir = path.join(outputDir, 'api', groupFolder);
176
+ baseName = 'api_group';
177
+ }
178
+ else if (doc.type === 'query' && doc.apiGroup) {
179
+ // query in group "test" → api/test/{query_name}.xs
180
+ const groupFolder = snakeCase(doc.apiGroup);
181
+ const nameParts = doc.name.split('/');
182
+ const leafName = nameParts.pop();
183
+ const folderParts = nameParts.map((part) => snakeCase(part));
184
+ typeDir = path.join(outputDir, 'api', groupFolder, ...folderParts);
185
+ baseName = this.sanitizeFilename(leafName);
186
+ if (doc.verb) {
187
+ baseName = `${baseName}_${doc.verb}`;
188
+ }
140
189
  }
141
- // Track duplicates per type directory
142
- if (!filenameCounters.has(doc.type)) {
143
- filenameCounters.set(doc.type, new Map());
190
+ else {
191
+ // Default: split folder path from name
192
+ const nameParts = doc.name.split('/');
193
+ const leafName = nameParts.pop();
194
+ const folderParts = nameParts.map((part) => snakeCase(part));
195
+ typeDir = path.join(outputDir, doc.type, ...folderParts);
196
+ baseName = this.sanitizeFilename(leafName);
197
+ if (doc.verb) {
198
+ baseName = `${baseName}_${doc.verb}`;
199
+ }
200
+ }
201
+ fs.mkdirSync(typeDir, { recursive: true });
202
+ // Track duplicates per directory
203
+ const dirKey = path.relative(outputDir, typeDir);
204
+ if (!filenameCounters.has(dirKey)) {
205
+ filenameCounters.set(dirKey, new Map());
144
206
  }
145
- const typeCounters = filenameCounters.get(doc.type);
207
+ const typeCounters = filenameCounters.get(dirKey);
146
208
  const count = typeCounters.get(baseName) || 0;
147
209
  typeCounters.set(baseName, count + 1);
148
210
  // Append numeric suffix for duplicates
149
211
  let filename;
150
- if (count === 0) {
151
- filename = `${baseName}.xs`;
152
- }
153
- else {
154
- filename = `${baseName}_${count + 1}.xs`;
155
- }
212
+ filename = count === 0 ? `${baseName}.xs` : `${baseName}_${count + 1}.xs`;
156
213
  const filePath = path.join(typeDir, filename);
157
214
  fs.writeFileSync(filePath, doc.content, 'utf8');
158
215
  writtenCount++;
159
216
  }
160
217
  this.log(`Pulled ${writtenCount} documents to ${args.directory}`);
161
218
  }
219
+ loadCredentials() {
220
+ const configDir = path.join(os.homedir(), '.xano');
221
+ const credentialsPath = path.join(configDir, 'credentials.yaml');
222
+ // Check if credentials file exists
223
+ if (!fs.existsSync(credentialsPath)) {
224
+ this.error(`Credentials file not found at ${credentialsPath}\n` +
225
+ `Create a profile using 'xano profile:create'`);
226
+ }
227
+ // Read credentials file
228
+ try {
229
+ const fileContent = fs.readFileSync(credentialsPath, 'utf8');
230
+ const parsed = yaml.load(fileContent);
231
+ if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
232
+ this.error('Credentials file has invalid format.');
233
+ }
234
+ return parsed;
235
+ }
236
+ catch (error) {
237
+ this.error(`Failed to parse credentials file: ${error}`);
238
+ }
239
+ }
162
240
  /**
163
241
  * Parse a single document to extract its type, name, and optional verb.
164
242
  * Skips leading comment lines (starting with //) to find the first
@@ -201,7 +279,13 @@ Pulled 58 documents to ./backup
201
279
  if (verbMatch) {
202
280
  verb = verbMatch[1];
203
281
  }
204
- return { type, name, verb, content };
282
+ // Extract api_group if present (e.g., api_group = "test")
283
+ let apiGroup;
284
+ const apiGroupMatch = content.match(/api_group\s*=\s*"([^"]*)"/);
285
+ if (apiGroupMatch) {
286
+ apiGroup = apiGroupMatch[1];
287
+ }
288
+ return { apiGroup, content, name, type, verb };
205
289
  }
206
290
  /**
207
291
  * Sanitize a document name for use as a filename.
@@ -209,30 +293,6 @@ Pulled 58 documents to ./backup
209
293
  * characters that are unsafe in filenames.
210
294
  */
211
295
  sanitizeFilename(name) {
212
- return name
213
- .replace(/"/g, '')
214
- .replace(/\s+/g, '_')
215
- .replace(/[<>:"/\\|?*]/g, '_');
216
- }
217
- loadCredentials() {
218
- const configDir = path.join(os.homedir(), '.xano');
219
- const credentialsPath = path.join(configDir, 'credentials.yaml');
220
- // Check if credentials file exists
221
- if (!fs.existsSync(credentialsPath)) {
222
- this.error(`Credentials file not found at ${credentialsPath}\n` +
223
- `Create a profile using 'xano profile:create'`);
224
- }
225
- // Read credentials file
226
- try {
227
- const fileContent = fs.readFileSync(credentialsPath, 'utf8');
228
- const parsed = yaml.load(fileContent);
229
- if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
230
- this.error('Credentials file has invalid format.');
231
- }
232
- return parsed;
233
- }
234
- catch (error) {
235
- this.error(`Failed to parse credentials file: ${error}`);
236
- }
296
+ return snakeCase(name.replaceAll('"', ''));
237
297
  }
238
298
  }
@@ -6,8 +6,10 @@ export default class Push extends BaseCommand {
6
6
  static description: string;
7
7
  static examples: string[];
8
8
  static flags: {
9
+ branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
10
  workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
11
  profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
13
  };
12
14
  run(): Promise<void>;
13
15
  /**
@@ -1,8 +1,8 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
+ import * as yaml from 'js-yaml';
2
3
  import * as fs from 'node:fs';
3
4
  import * as os from 'node:os';
4
5
  import * as path from 'node:path';
5
- import * as yaml from 'js-yaml';
6
6
  import BaseCommand from '../../../base-command.js';
7
7
  export default class Push extends BaseCommand {
8
8
  static args = {
@@ -21,10 +21,18 @@ Pushed 15 documents from ./output
21
21
  `,
22
22
  `$ xano workspace push ./backup --profile production
23
23
  Pushed 58 documents from ./backup
24
+ `,
25
+ `$ xano workspace push ./my-workspace -b dev
26
+ Pushed 42 documents from ./my-workspace
24
27
  `,
25
28
  ];
26
29
  static flags = {
27
30
  ...BaseCommand.baseFlags,
31
+ branch: Flags.string({
32
+ char: 'b',
33
+ description: 'Branch name (optional if set in profile, defaults to live)',
34
+ required: false,
35
+ }),
28
36
  workspace: Flags.string({
29
37
  char: 'w',
30
38
  description: 'Workspace ID (optional if set in profile)',
@@ -88,18 +96,21 @@ Pushed 58 documents from ./backup
88
96
  this.error(`All .xs files in ${args.directory} are empty`);
89
97
  }
90
98
  const multidoc = documents.join('\n---\n');
99
+ // Determine branch from flag or profile
100
+ const branch = flags.branch || profile.branch || '';
91
101
  // Construct the API URL
92
- const apiUrl = `${profile.instance_origin}/api:meta/beta/workspace/${workspaceId}/multidoc`;
102
+ const queryParams = new URLSearchParams({ branch });
103
+ const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/multidoc?${queryParams.toString()}`;
93
104
  // POST the multidoc to the API
94
105
  try {
95
106
  const response = await fetch(apiUrl, {
96
- method: 'POST',
107
+ body: multidoc,
97
108
  headers: {
98
109
  'accept': 'application/json',
99
110
  'Authorization': `Bearer ${profile.access_token}`,
100
111
  'Content-Type': 'text/x-xanoscript',
101
112
  },
102
- body: multidoc,
113
+ method: 'POST',
103
114
  });
104
115
  if (!response.ok) {
105
116
  const errorText = await response.text();
@@ -107,7 +118,7 @@ Pushed 58 documents from ./backup
107
118
  }
108
119
  // Log the response if any
109
120
  const responseText = await response.text();
110
- if (responseText) {
121
+ if (responseText && responseText !== 'null') {
111
122
  this.log(responseText);
112
123
  }
113
124
  }
package/dist/help.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Command, Help as BaseHelp } from '@oclif/core';
1
+ import { Help as BaseHelp, Command } from '@oclif/core';
2
2
  import { CommandHelp as BaseCommandHelp } from '@oclif/core/help';
3
3
  /**
4
4
  * Custom CommandHelp class that extends the default to display environment variables
@@ -4,19 +4,19 @@
4
4
  import BaseCommand from '../base-command.js';
5
5
  import { RunHttpClient } from './run-http-client.js';
6
6
  export interface ProfileConfig {
7
- account_origin?: string;
8
- instance_origin: string;
9
7
  access_token: string;
10
- workspace?: string;
8
+ account_origin?: string;
11
9
  branch?: string;
10
+ instance_origin: string;
12
11
  project?: string;
13
12
  run_base_url?: string;
13
+ workspace?: string;
14
14
  }
15
15
  export interface CredentialsFile {
16
+ default?: string;
16
17
  profiles: {
17
18
  [key: string]: ProfileConfig;
18
19
  };
19
- default?: string;
20
20
  }
21
21
  export default abstract class BaseRunCommand extends BaseCommand {
22
22
  protected httpClient: RunHttpClient;
@@ -25,11 +25,11 @@ export default abstract class BaseRunCommand extends BaseCommand {
25
25
  /**
26
26
  * Initialize the run command with profile and HTTP client
27
27
  */
28
- protected initRunCommand(profileFlag?: string): Promise<void>;
28
+ protected initRunCommand(profileFlag?: string, verbose?: boolean): Promise<void>;
29
29
  /**
30
30
  * Initialize with project required
31
31
  */
32
- protected initRunCommandWithProject(profileFlag?: string): Promise<void>;
32
+ protected initRunCommandWithProject(profileFlag?: string, verbose?: boolean): Promise<void>;
33
33
  /**
34
34
  * Load credentials from file
35
35
  */