@xano/cli 0.0.95-beta.23 → 0.0.95-beta.25

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.
@@ -1,36 +1,106 @@
1
- import { Args, Flags, ux } from '@oclif/core';
1
+ import { Flags } from '@oclif/core';
2
2
  import * as fs from 'node:fs';
3
- import * as path from 'node:path';
3
+ import { resolve } from 'node:path';
4
+ import open from 'open';
4
5
  import BaseCommand from '../../../base-command.js';
5
- import { findFilesWithGuid } from '../../../utils/document-parser.js';
6
- import { checkReferences, checkTableIndexes } from '../../../utils/reference-checker.js';
6
+ import { executePush } from '../../../utils/multidoc-push.js';
7
7
  export default class SandboxPush extends BaseCommand {
8
- static args = {
9
- directory: Args.string({
10
- description: 'Directory containing documents to push (as produced by sandbox pull or workspace pull)',
11
- required: true,
12
- }),
13
- };
14
- static description = 'Push local documents to your sandbox environment via multidoc import';
8
+ static description = 'Push local documents to your sandbox environment via multidoc import. By default, only changed files are pushed (partial mode). Use --sync to push all files. Shows a preview of changes before pushing unless --force is specified. Use --dry-run to preview only.';
15
9
  static examples = [
16
- `$ xano sandbox push ./my-workspace
17
- Pushed 42 documents to sandbox environment from ./my-workspace
10
+ `$ xano sandbox push
11
+ Push from current directory (default partial mode)
12
+ `,
13
+ `$ xano sandbox push -d ./my-workspace
14
+ Push from a specific directory
15
+ `,
16
+ `$ xano sandbox push --sync
17
+ Push all files to the sandbox
18
+ `,
19
+ `$ xano sandbox push --sync --delete
20
+ Push all files and delete remote objects not included
21
+ `,
22
+ `$ xano sandbox push --dry-run
23
+ Preview changes without pushing
24
+ `,
25
+ `$ xano sandbox push --force
26
+ Skip preview and push immediately
27
+ `,
28
+ `$ xano sandbox push --records --env`,
29
+ `$ xano sandbox push --truncate`,
30
+ `$ xano sandbox push -i "**/func*"
31
+ Push only files matching the glob pattern
32
+ `,
33
+ `$ xano sandbox push -i "function/*" -i "table/*"
34
+ Push files matching multiple patterns
35
+ `,
36
+ `$ xano sandbox push -e "table/*"
37
+ Push all files except tables
38
+ `,
39
+ `$ xano sandbox push --review
40
+ Push and open sandbox review in the browser
18
41
  `,
19
- `$ xano sandbox push ./backup --records --env`,
20
- `$ xano sandbox push ./my-workspace --truncate`,
21
42
  ];
22
43
  static flags = {
23
44
  ...BaseCommand.baseFlags,
45
+ directory: Flags.string({
46
+ char: 'd',
47
+ default: '.',
48
+ description: 'Directory containing documents to push (defaults to current directory)',
49
+ required: false,
50
+ }),
51
+ delete: Flags.boolean({
52
+ default: false,
53
+ description: 'Delete sandbox objects not included in the push (requires --sync)',
54
+ required: false,
55
+ }),
56
+ 'dry-run': Flags.boolean({
57
+ default: false,
58
+ description: 'Show preview of changes without pushing (exit after preview)',
59
+ required: false,
60
+ }),
24
61
  env: Flags.boolean({
25
62
  default: false,
26
63
  description: 'Include environment variables in import',
27
64
  required: false,
28
65
  }),
66
+ exclude: Flags.string({
67
+ char: 'e',
68
+ description: 'Glob pattern to exclude files (e.g. "table/*", "**/test*"). Matched against relative paths from the push directory.',
69
+ multiple: true,
70
+ required: false,
71
+ }),
72
+ force: Flags.boolean({
73
+ default: false,
74
+ description: 'Skip preview and confirmation prompt (for CI/CD pipelines)',
75
+ required: false,
76
+ }),
77
+ guids: Flags.boolean({
78
+ allowNo: true,
79
+ default: true,
80
+ description: 'Write server-assigned GUIDs back to local files (use --no-guids to skip)',
81
+ required: false,
82
+ }),
83
+ include: Flags.string({
84
+ char: 'i',
85
+ description: 'Glob pattern to include files (e.g. "**/func*", "table/*.xs"). Matched against relative paths from the push directory.',
86
+ multiple: true,
87
+ required: false,
88
+ }),
29
89
  records: Flags.boolean({
30
90
  default: false,
31
91
  description: 'Include records in import',
32
92
  required: false,
33
93
  }),
94
+ review: Flags.boolean({
95
+ default: false,
96
+ description: 'Open sandbox review in the browser after pushing',
97
+ required: false,
98
+ }),
99
+ sync: Flags.boolean({
100
+ default: false,
101
+ description: 'Full push — send all files, not just changed ones. Required for --delete.',
102
+ required: false,
103
+ }),
34
104
  transaction: Flags.boolean({
35
105
  allowNo: true,
36
106
  default: true,
@@ -44,140 +114,83 @@ Pushed 42 documents to sandbox environment from ./my-workspace
44
114
  }),
45
115
  };
46
116
  async run() {
47
- const { args, flags } = await this.parse(SandboxPush);
117
+ const { flags } = await this.parse(SandboxPush);
48
118
  const { profile } = this.resolveProfile(flags);
49
- const inputDir = path.resolve(args.directory);
119
+ const inputDir = resolve(flags.directory);
50
120
  if (!fs.existsSync(inputDir)) {
51
121
  this.error(`Directory not found: ${inputDir}`);
52
122
  }
53
123
  if (!fs.statSync(inputDir).isDirectory()) {
54
124
  this.error(`Not a directory: ${inputDir}`);
55
125
  }
56
- const files = this.collectFiles(inputDir);
57
- if (files.length === 0) {
58
- this.error(`No .xs files found in ${args.directory}`);
59
- }
60
- const documentEntries = [];
61
- for (const filePath of files) {
62
- const content = fs.readFileSync(filePath, 'utf8').trim();
63
- if (content) {
64
- documentEntries.push({ content, filePath });
65
- }
66
- }
67
- if (documentEntries.length === 0) {
68
- this.error(`All .xs files in ${args.directory} are empty`);
126
+ const baseUrl = `${profile.instance_origin}/api:meta/sandbox`;
127
+ const target = {
128
+ buildDryRunUrl: (params) => `${baseUrl}/multidoc/dry-run?${params.toString()}`,
129
+ buildPushUrl: (params) => `${baseUrl}/multidoc?${params.toString()}`,
130
+ cliVersion: this.config.version,
131
+ instanceOrigin: profile.instance_origin,
132
+ label: 'sandbox environment',
133
+ supportsBranches: false,
134
+ supportsPartial: false,
135
+ };
136
+ const pushFlags = {
137
+ delete: flags.delete,
138
+ 'dry-run': flags['dry-run'],
139
+ env: flags.env,
140
+ exclude: flags.exclude,
141
+ force: flags.force,
142
+ guids: flags.guids,
143
+ include: flags.include,
144
+ records: flags.records,
145
+ sync: flags.sync,
146
+ transaction: flags.transaction,
147
+ truncate: flags.truncate,
148
+ verbose: flags.verbose,
149
+ };
150
+ await executePush({
151
+ accessToken: profile.access_token,
152
+ branch: '',
153
+ command: this,
154
+ inputDir,
155
+ verboseFetch: this.verboseFetch.bind(this),
156
+ }, target, pushFlags);
157
+ if (flags.review) {
158
+ await this.openReview(profile.instance_origin, profile.access_token, flags.verbose);
69
159
  }
70
- // Check for bad cross-references within the local file set
71
- const badRefs = checkReferences(documentEntries);
72
- if (badRefs.length > 0) {
73
- this.renderBadReferences(badRefs);
160
+ }
161
+ async openReview(instanceOrigin, accessToken, verbose) {
162
+ const response = await this.verboseFetch(`${instanceOrigin}/api:meta/sandbox/impersonate`, {
163
+ headers: {
164
+ accept: 'application/json',
165
+ Authorization: `Bearer ${accessToken}`,
166
+ },
167
+ method: 'GET',
168
+ }, verbose, accessToken);
169
+ if (!response.ok) {
170
+ const message = await this.parseApiError(response, 'Failed to open sandbox review');
171
+ this.error(message);
74
172
  }
75
- // Check for indexes referencing non-existent schema fields
76
- const badIndexes = checkTableIndexes(documentEntries);
77
- if (badIndexes.length > 0) {
78
- this.renderBadIndexes(badIndexes);
173
+ const result = (await response.json());
174
+ if (!result._ti) {
175
+ this.error('No one-time token returned from impersonate API');
79
176
  }
80
- const multidoc = documentEntries.map((d) => d.content).join('\n---\n');
81
- const queryParams = new URLSearchParams({
82
- env: flags.env.toString(),
83
- records: flags.records.toString(),
84
- transaction: flags.transaction.toString(),
85
- truncate: flags.truncate.toString(),
86
- });
87
- const apiUrl = `${profile.instance_origin}/api:meta/sandbox/multidoc?${queryParams.toString()}`;
88
- const startTime = Date.now();
89
- try {
90
- const response = await this.verboseFetch(apiUrl, {
91
- body: multidoc,
92
- headers: {
93
- accept: 'application/json',
94
- Authorization: `Bearer ${profile.access_token}`,
95
- 'Content-Type': 'text/x-xanoscript',
96
- },
97
- method: 'POST',
98
- }, flags.verbose, profile.access_token);
99
- if (!response.ok) {
100
- const errorText = await response.text();
101
- let errorMessage = `Push failed (${response.status})`;
102
- try {
103
- const errorJson = JSON.parse(errorText);
104
- errorMessage += `: ${errorJson.message}`;
105
- if (errorJson.payload?.param) {
106
- errorMessage += `\n Parameter: ${errorJson.payload.param}`;
107
- }
108
- }
109
- catch {
110
- errorMessage += `\n${errorText}`;
111
- }
112
- // Provide guidance when sandbox access is denied (free plan restriction)
113
- if (response.status === 500 && errorMessage.includes('Access Denied')) {
114
- this.error('Sandbox is not available on the Free plan. Upgrade your plan to use sandbox features.');
115
- }
116
- const guidMatch = errorMessage.match(/Duplicate \w+ guid: (\S+)/);
117
- if (guidMatch) {
118
- const dupeFiles = findFilesWithGuid(documentEntries, guidMatch[1]);
119
- if (dupeFiles.length > 0) {
120
- const relPaths = dupeFiles.map((f) => path.relative(inputDir, f));
121
- errorMessage += `\n Local files with this GUID:\n${relPaths.map((f) => ` ${f}`).join('\n')}`;
122
- }
123
- }
124
- this.error(errorMessage);
125
- }
126
- const responseText = await response.text();
127
- if (responseText && responseText !== 'null' && flags.verbose) {
128
- this.log(responseText);
129
- }
130
- }
131
- catch (error) {
132
- if (error instanceof Error && 'oclif' in error)
133
- throw error;
134
- if (error instanceof Error) {
135
- this.error(`Failed to push multidoc: ${error.message}`);
136
- }
137
- else {
138
- this.error(`Failed to push multidoc: ${String(error)}`);
139
- }
140
- }
141
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
142
- this.log(`Pushed ${documentEntries.length} documents to sandbox environment from ${args.directory} in ${elapsed}s`);
177
+ const frontendUrl = this.getFrontendUrl(instanceOrigin);
178
+ const params = new URLSearchParams({ _ti: result._ti });
179
+ const reviewUrl = `${frontendUrl}/impersonate?${params.toString()}`;
180
+ this.log('Opening sandbox review...');
181
+ await open(reviewUrl);
143
182
  }
144
- collectFiles(dir) {
145
- const files = [];
146
- const entries = fs.readdirSync(dir, { withFileTypes: true });
147
- for (const entry of entries) {
148
- const fullPath = path.join(dir, entry.name);
149
- if (entry.isDirectory()) {
150
- files.push(...this.collectFiles(fullPath));
151
- }
152
- else if (entry.isFile() && entry.name.endsWith('.xs')) {
153
- files.push(fullPath);
183
+ getFrontendUrl(instanceOrigin) {
184
+ try {
185
+ const url = new URL(instanceOrigin);
186
+ if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
187
+ url.port = '4200';
188
+ return url.origin;
154
189
  }
155
190
  }
156
- return files.sort();
157
- }
158
- renderBadIndexes(badIndexes) {
159
- this.log('');
160
- this.log(ux.colorize('red', ux.colorize('bold', '=== CRITICAL: Invalid Indexes ===')));
161
- this.log('');
162
- this.log(ux.colorize('red', 'The following tables have indexed referencing fields that do not exist in the schema, which may cause related issues.'));
163
- this.log('');
164
- for (const idx of badIndexes) {
165
- this.log(` ${ux.colorize('red', 'CRITICAL'.padEnd(16))} ${'table'.padEnd(18)} ${idx.table}`);
166
- this.log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', `${idx.indexType} index → field "${idx.field}" does not exist in schema`)}`);
167
- }
168
- this.log('');
169
- }
170
- renderBadReferences(badRefs) {
171
- this.log('');
172
- this.log(ux.colorize('yellow', ux.colorize('bold', '=== Unresolved References ===')));
173
- this.log('');
174
- this.log(ux.colorize('yellow', "The following references point to objects that don't exist in this push or on the server."));
175
- this.log(ux.colorize('yellow', 'These will become placeholder statements after import.'));
176
- this.log('');
177
- for (const ref of badRefs) {
178
- this.log(` ${ux.colorize('yellow', 'WARNING'.padEnd(16))} ${ref.sourceType.padEnd(18)} ${ref.source}`);
179
- this.log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', `${ref.statementType} → ${ref.targetType} "${ref.target}" does not exist`)}`);
191
+ catch {
192
+ // fall through
180
193
  }
181
- this.log('');
194
+ return instanceOrigin;
182
195
  }
183
196
  }
@@ -1,11 +1,9 @@
1
1
  import BaseCommand from '../../../base-command.js';
2
2
  export default class Pull extends BaseCommand {
3
- static args: {
4
- directory: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
- };
6
3
  static description: string;
7
4
  static examples: string[];
8
5
  static flags: {
6
+ directory: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
7
  draft: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
8
  env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
9
  records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
@@ -1,4 +1,4 @@
1
- import { Args, Flags } from '@oclif/core';
1
+ import { Flags } from '@oclif/core';
2
2
  import * as yaml from 'js-yaml';
3
3
  import * as fs from 'node:fs';
4
4
  import * as path from 'node:path';
@@ -6,29 +6,30 @@ import snakeCase from 'lodash.snakecase';
6
6
  import BaseCommand from '../../../base-command.js';
7
7
  import { buildApiGroupFolderResolver, parseDocument } from '../../../utils/document-parser.js';
8
8
  export default class Pull extends BaseCommand {
9
- static args = {
10
- directory: Args.string({
11
- description: 'Output directory for pulled documents',
12
- required: true,
13
- }),
14
- };
15
9
  static description = 'Pull a tenant multidoc from the Xano Metadata API and split into individual files';
16
10
  static examples = [
17
- `$ xano tenant pull ./my-tenant -t my-tenant
11
+ `$ xano tenant pull -t my-tenant
12
+ Pulled 42 documents from tenant my-tenant to current directory
13
+ `,
14
+ `$ xano tenant pull -d ./my-tenant -t my-tenant
18
15
  Pulled 42 documents from tenant my-tenant to ./my-tenant
19
16
  `,
20
- `$ xano tenant pull ./output -t my-tenant -w 40
17
+ `$ xano tenant pull -d ./output -t my-tenant -w 40
21
18
  Pulled 15 documents from tenant my-tenant to ./output
22
19
  `,
23
- `$ xano tenant pull ./backup -t my-tenant --profile production --env --records
24
- Pulled 58 documents from tenant my-tenant to ./backup
25
- `,
26
- `$ xano tenant pull ./my-tenant -t my-tenant --draft
27
- Pulled 42 documents from tenant my-tenant to ./my-tenant
20
+ `$ xano tenant pull -t my-tenant --profile production --env --records
21
+ Pulled 58 documents from tenant my-tenant
28
22
  `,
23
+ `$ xano tenant pull -t my-tenant --draft`,
29
24
  ];
30
25
  static flags = {
31
26
  ...BaseCommand.baseFlags,
27
+ directory: Flags.string({
28
+ char: 'd',
29
+ default: '.',
30
+ description: 'Output directory for pulled documents (defaults to current directory)',
31
+ required: false,
32
+ }),
32
33
  draft: Flags.boolean({
33
34
  default: false,
34
35
  description: 'Include draft versions',
@@ -56,7 +57,7 @@ Pulled 42 documents from tenant my-tenant to ./my-tenant
56
57
  }),
57
58
  };
58
59
  async run() {
59
- const { args, flags } = await this.parse(Pull);
60
+ const { flags } = await this.parse(Pull);
60
61
  // Get profile name (default or from flag/env)
61
62
  const profileName = flags.profile || this.getDefaultProfile();
62
63
  // Load credentials
@@ -84,7 +85,7 @@ Pulled 42 documents from tenant my-tenant to ./my-tenant
84
85
  }
85
86
  else {
86
87
  this.error(`Workspace ID is required. Either:\n` +
87
- ` 1. Provide it as a flag: xano tenant pull <directory> -t <tenant_name> -w <workspace_id>\n` +
88
+ ` 1. Provide it as a flag: xano tenant pull -t <tenant_name> -w <workspace_id>\n` +
88
89
  ` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
89
90
  }
90
91
  const tenantName = flags.tenant;
@@ -140,7 +141,7 @@ Pulled 42 documents from tenant my-tenant to ./my-tenant
140
141
  return;
141
142
  }
142
143
  // Resolve the output directory
143
- const outputDir = path.resolve(args.directory);
144
+ const outputDir = path.resolve(flags.directory);
144
145
  // Create the output directory if it doesn't exist
145
146
  fs.mkdirSync(outputDir, { recursive: true });
146
147
  // Resolve api_group names to unique folder names, disambiguating collisions
@@ -246,7 +247,7 @@ Pulled 42 documents from tenant my-tenant to ./my-tenant
246
247
  fs.writeFileSync(filePath, doc.content, 'utf8');
247
248
  writtenCount++;
248
249
  }
249
- this.log(`Pulled ${writtenCount} documents from tenant ${tenantName} to ${args.directory}`);
250
+ this.log(`Pulled ${writtenCount} documents from tenant ${tenantName} to ${flags.directory}`);
250
251
  }
251
252
  loadCredentials() {
252
253
  const credentialsPath = this.getCredentialsPath();
@@ -1,12 +1,10 @@
1
1
  import BaseCommand from '../../../../base-command.js';
2
2
  export default class GitPull extends BaseCommand {
3
- static args: {
4
- directory: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
- };
6
3
  static description: string;
7
4
  static examples: string[];
8
5
  static flags: {
9
6
  branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ directory: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
8
  path: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
9
  repo: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
10
  token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -1,4 +1,4 @@
1
- import { Args, Flags } from '@oclif/core';
1
+ import { Flags } from '@oclif/core';
2
2
  import { execSync } from 'node:child_process';
3
3
  import * as fs from 'node:fs';
4
4
  import * as os from 'node:os';
@@ -7,21 +7,16 @@ import snakeCase from 'lodash.snakecase';
7
7
  import BaseCommand, { buildUserAgent } from '../../../../base-command.js';
8
8
  import { buildApiGroupFolderResolver, parseDocument } from '../../../../utils/document-parser.js';
9
9
  export default class GitPull extends BaseCommand {
10
- static args = {
11
- directory: Args.string({
12
- description: 'Output directory for imported files',
13
- required: true,
14
- }),
15
- };
16
10
  static description = 'Pull XanoScript files from a git repository into a local directory';
17
11
  static examples = [
18
- `$ xano workspace git pull ./output -r https://github.com/owner/repo`,
19
- `$ xano workspace git pull ./output -r https://github.com/owner/repo/tree/main/path/to/dir`,
20
- `$ xano workspace git pull ./output -r https://github.com/owner/repo/blob/main/path/to/file.xs`,
21
- `$ xano workspace git pull ./output -r git@github.com:owner/repo.git`,
22
- `$ xano workspace git pull ./output -r https://github.com/owner/private-repo -t ghp_xxx`,
23
- `$ xano workspace git pull ./output -r https://gitlab.com/owner/repo/-/tree/master/path`,
24
- `$ xano workspace git pull ./output -r https://gitlab.com/owner/repo -b main`,
12
+ `$ xano workspace git pull -r https://github.com/owner/repo`,
13
+ `$ xano workspace git pull -d ./output -r https://github.com/owner/repo`,
14
+ `$ xano workspace git pull -r https://github.com/owner/repo/tree/main/path/to/dir`,
15
+ `$ xano workspace git pull -r https://github.com/owner/repo/blob/main/path/to/file.xs`,
16
+ `$ xano workspace git pull -r git@github.com:owner/repo.git`,
17
+ `$ xano workspace git pull -r https://github.com/owner/private-repo -t ghp_xxx`,
18
+ `$ xano workspace git pull -r https://gitlab.com/owner/repo/-/tree/master/path`,
19
+ `$ xano workspace git pull -r https://gitlab.com/owner/repo -b main`,
25
20
  ];
26
21
  static flags = {
27
22
  ...BaseCommand.baseFlags,
@@ -30,6 +25,12 @@ export default class GitPull extends BaseCommand {
30
25
  description: 'Branch, tag, or ref to fetch (defaults to repository default branch)',
31
26
  required: false,
32
27
  }),
28
+ directory: Flags.string({
29
+ char: 'd',
30
+ default: '.',
31
+ description: 'Output directory for imported files (defaults to current directory)',
32
+ required: false,
33
+ }),
33
34
  path: Flags.string({
34
35
  description: 'Subdirectory within the repo to import from',
35
36
  required: false,
@@ -47,9 +48,9 @@ export default class GitPull extends BaseCommand {
47
48
  }),
48
49
  };
49
50
  async run() {
50
- const { args, flags } = await this.parse(GitPull);
51
+ const { flags } = await this.parse(GitPull);
51
52
  const token = flags.token || '';
52
- const outputDir = path.resolve(args.directory);
53
+ const outputDir = path.resolve(flags.directory);
53
54
  // Normalize the URL to extract owner/repo/ref/path from various formats
54
55
  const repoInfo = this.parseRepoUrl(flags.repo);
55
56
  // CLI flags override values extracted from the URL
@@ -115,7 +116,7 @@ export default class GitPull extends BaseCommand {
115
116
  writtenCount++;
116
117
  }
117
118
  const source = subPath ? `${flags.repo} (${subPath})` : flags.repo;
118
- this.log(`Pulled ${writtenCount} documents from ${source} to ${args.directory}`);
119
+ this.log(`Pulled ${writtenCount} documents from ${source} to ${flags.directory}`);
119
120
  }
120
121
  finally {
121
122
  // Clean up temp directory
@@ -1,12 +1,10 @@
1
1
  import BaseCommand from '../../../base-command.js';
2
2
  export default class Pull extends BaseCommand {
3
- static args: {
4
- directory: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
- };
6
3
  static description: string;
7
4
  static examples: string[];
8
5
  static flags: {
9
6
  branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ directory: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
8
  env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
9
  draft: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
10
  records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
@@ -1,4 +1,4 @@
1
- import { Args, Flags } from '@oclif/core';
1
+ import { Flags } from '@oclif/core';
2
2
  import * as yaml from 'js-yaml';
3
3
  import * as fs from 'node:fs';
4
4
  import * as path from 'node:path';
@@ -6,29 +6,22 @@ import snakeCase from 'lodash.snakecase';
6
6
  import BaseCommand from '../../../base-command.js';
7
7
  import { buildApiGroupFolderResolver, parseDocument } from '../../../utils/document-parser.js';
8
8
  export default class Pull extends BaseCommand {
9
- static args = {
10
- directory: Args.string({
11
- description: 'Output directory for pulled documents',
12
- required: true,
13
- }),
14
- };
15
9
  static description = 'Pull a workspace multidoc from the Xano Metadata API and split into individual files';
16
10
  static examples = [
17
- `$ xano workspace pull ./my-workspace
11
+ `$ xano workspace pull
12
+ Pulled 42 documents to current directory
13
+ `,
14
+ `$ xano workspace pull -d ./my-workspace
18
15
  Pulled 42 documents to ./my-workspace
19
16
  `,
20
- `$ xano workspace pull ./output -w 40
17
+ `$ xano workspace pull -d ./output -w 40
21
18
  Pulled 15 documents to ./output
22
19
  `,
23
- `$ xano workspace pull ./backup --profile production --env --records
24
- Pulled 58 documents to ./backup
25
- `,
26
- `$ xano workspace pull ./my-workspace --draft
27
- Pulled 42 documents to ./my-workspace
28
- `,
29
- `$ xano workspace pull ./my-workspace -b dev
30
- Pulled 42 documents to ./my-workspace
20
+ `$ xano workspace pull --profile production --env --records
21
+ Pulled 58 documents
31
22
  `,
23
+ `$ xano workspace pull --draft`,
24
+ `$ xano workspace pull -b dev`,
32
25
  ];
33
26
  static flags = {
34
27
  ...BaseCommand.baseFlags,
@@ -37,6 +30,12 @@ Pulled 42 documents to ./my-workspace
37
30
  description: 'Branch name (optional if set in profile, defaults to live)',
38
31
  required: false,
39
32
  }),
33
+ directory: Flags.string({
34
+ char: 'd',
35
+ default: '.',
36
+ description: 'Output directory for pulled documents (defaults to current directory)',
37
+ required: false,
38
+ }),
40
39
  env: Flags.boolean({
41
40
  default: false,
42
41
  description: 'Include environment variables',
@@ -59,7 +58,7 @@ Pulled 42 documents to ./my-workspace
59
58
  }),
60
59
  };
61
60
  async run() {
62
- const { args, flags } = await this.parse(Pull);
61
+ const { flags } = await this.parse(Pull);
63
62
  // Get profile name (default or from flag/env)
64
63
  const profileName = flags.profile || this.getDefaultProfile();
65
64
  // Load credentials
@@ -87,7 +86,7 @@ Pulled 42 documents to ./my-workspace
87
86
  }
88
87
  else {
89
88
  this.error(`Workspace ID is required. Either:\n` +
90
- ` 1. Provide it as a flag: xano workspace pull <directory> -w <workspace_id>\n` +
89
+ ` 1. Provide it as a flag: xano workspace pull -w <workspace_id>\n` +
91
90
  ` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
92
91
  }
93
92
  // Determine branch from flag or profile
@@ -145,7 +144,7 @@ Pulled 42 documents to ./my-workspace
145
144
  return;
146
145
  }
147
146
  // Resolve the output directory
148
- const outputDir = path.resolve(args.directory);
147
+ const outputDir = path.resolve(flags.directory);
149
148
  // Create the output directory if it doesn't exist
150
149
  fs.mkdirSync(outputDir, { recursive: true });
151
150
  // Resolve api_group names to unique folder names, disambiguating collisions
@@ -252,7 +251,7 @@ Pulled 42 documents to ./my-workspace
252
251
  fs.writeFileSync(filePath, doc.content, 'utf8');
253
252
  writtenCount++;
254
253
  }
255
- this.log(`Pulled ${writtenCount} documents to ${args.directory}`);
254
+ this.log(`Pulled ${writtenCount} documents to ${flags.directory}`);
256
255
  }
257
256
  loadCredentials() {
258
257
  const credentialsPath = this.getCredentialsPath();