@xano/cli 0.0.95-beta.22 → 0.0.95-beta.24

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,11 +1,9 @@
1
1
  import BaseCommand from '../../../base-command.js';
2
2
  export default class ReleasePull 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
  env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
8
  records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
9
  release: import("@oclif/core/interfaces").OptionFlag<string, 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 * as yaml from 'js-yaml';
3
3
  import * as fs from 'node:fs';
4
4
  import * as path from 'node:path';
@@ -6,26 +6,29 @@ 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 ReleasePull 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 release multidoc from the Xano Metadata API and split into individual files';
16
10
  static examples = [
17
- `$ xano release pull ./my-release -r v1.0
11
+ `$ xano release pull -r v1.0
12
+ Pulled 42 documents from release 'v1.0' to current directory
13
+ `,
14
+ `$ xano release pull -d ./my-release -r v1.0
18
15
  Pulled 42 documents from release 'v1.0' to ./my-release
19
16
  `,
20
- `$ xano release pull ./output -r v1.0 -w 40
17
+ `$ xano release pull -d ./output -r v1.0 -w 40
21
18
  Pulled 15 documents from release 'v1.0' to ./output
22
19
  `,
23
- `$ xano release pull ./backup -r v1.0 --profile production --env --records
24
- Pulled 58 documents from release 'v1.0' to ./backup
20
+ `$ xano release pull -r v1.0 --profile production --env --records
21
+ Pulled 58 documents from release 'v1.0'
25
22
  `,
26
23
  ];
27
24
  static flags = {
28
25
  ...BaseCommand.baseFlags,
26
+ directory: Flags.string({
27
+ char: 'd',
28
+ default: '.',
29
+ description: 'Output directory for pulled documents (defaults to current directory)',
30
+ required: false,
31
+ }),
29
32
  env: Flags.boolean({
30
33
  default: false,
31
34
  description: 'Include environment variables',
@@ -48,7 +51,7 @@ Pulled 58 documents from release 'v1.0' to ./backup
48
51
  }),
49
52
  };
50
53
  async run() {
51
- const { args, flags } = await this.parse(ReleasePull);
54
+ const { flags } = await this.parse(ReleasePull);
52
55
  // Get profile name (default or from flag/env)
53
56
  const profileName = flags.profile || this.getDefaultProfile();
54
57
  // Load credentials
@@ -76,7 +79,7 @@ Pulled 58 documents from release 'v1.0' to ./backup
76
79
  }
77
80
  else {
78
81
  this.error(`Workspace ID is required. Either:\n` +
79
- ` 1. Provide it as a flag: xano release pull <directory> -r <release_name> -w <workspace_id>\n` +
82
+ ` 1. Provide it as a flag: xano release pull -r <release_name> -w <workspace_id>\n` +
80
83
  ` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
81
84
  }
82
85
  const releaseName = flags.release;
@@ -132,7 +135,7 @@ Pulled 58 documents from release 'v1.0' to ./backup
132
135
  return;
133
136
  }
134
137
  // Resolve the output directory
135
- const outputDir = path.resolve(args.directory);
138
+ const outputDir = path.resolve(flags.directory);
136
139
  // Create the output directory if it doesn't exist
137
140
  fs.mkdirSync(outputDir, { recursive: true });
138
141
  // Resolve api_group names to unique folder names, disambiguating collisions
@@ -238,7 +241,7 @@ Pulled 58 documents from release 'v1.0' to ./backup
238
241
  fs.writeFileSync(filePath, doc.content, 'utf8');
239
242
  writtenCount++;
240
243
  }
241
- this.log(`Pulled ${writtenCount} documents from release '${releaseName}' to ${args.directory}`);
244
+ this.log(`Pulled ${writtenCount} documents from release '${releaseName}' to ${flags.directory}`);
242
245
  }
243
246
  loadCredentials() {
244
247
  const credentialsPath = this.getCredentialsPath();
@@ -1,12 +1,10 @@
1
1
  import BaseCommand from '../../../base-command.js';
2
2
  export default class ReleasePush 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
  description: import("@oclif/core/interfaces").OptionFlag<string, 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
  hotfix: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
10
  name: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
@@ -1,42 +1,41 @@
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';
5
5
  import BaseCommand from '../../../base-command.js';
6
6
  import { findFilesWithGuid } from '../../../utils/document-parser.js';
7
7
  export default class ReleasePush extends BaseCommand {
8
- static args = {
9
- directory: Args.string({
10
- description: 'Directory containing .xs documents to create the release from',
11
- required: true,
12
- }),
13
- };
14
8
  static description = 'Create a new release from local XanoScript files via the multidoc endpoint';
15
9
  static examples = [
16
- `$ xano release push ./my-release -n "v1.0"
17
- Created release: v1.0 - ID: 10
10
+ `$ xano release push -n "v1.0"
11
+ Created release: v1.0 - ID: 10 (from current directory)
18
12
  `,
19
- `$ xano release push ./output -n "v2.0" -w 40 -d "Major update"
13
+ `$ xano release push -d ./output -n "v2.0" -w 40 --description "Major update"
20
14
  Created release: v2.0 - ID: 15
21
15
  `,
22
- `$ xano release push ./backup -n "v1.1-hotfix" --hotfix --profile production
16
+ `$ xano release push -d ./backup -n "v1.1-hotfix" --hotfix --profile production
23
17
  Created release: v1.1-hotfix - ID: 20
24
18
  `,
25
- `$ xano release push ./my-release -n "v1.0" --no-records --no-env
19
+ `$ xano release push -n "v1.0" --no-records --no-env
26
20
  Create release from schema only, skip records and environment variables
27
21
  `,
28
- `$ xano release push ./my-release -n "v1.0" -o json
22
+ `$ xano release push -n "v1.0" -o json
29
23
  Output release details as JSON
30
24
  `,
31
25
  ];
32
26
  static flags = {
33
27
  ...BaseCommand.baseFlags,
34
28
  description: Flags.string({
35
- char: 'd',
36
29
  default: '',
37
30
  description: 'Release description',
38
31
  required: false,
39
32
  }),
33
+ directory: Flags.string({
34
+ char: 'd',
35
+ default: '.',
36
+ description: 'Directory containing .xs documents to create the release from (defaults to current directory)',
37
+ required: false,
38
+ }),
40
39
  env: Flags.boolean({
41
40
  allowNo: true,
42
41
  default: true,
@@ -73,7 +72,7 @@ Output release details as JSON
73
72
  }),
74
73
  };
75
74
  async run() {
76
- const { args, flags } = await this.parse(ReleasePush);
75
+ const { flags } = await this.parse(ReleasePush);
77
76
  // Get profile name (default or from flag/env)
78
77
  const profileName = flags.profile || this.getDefaultProfile();
79
78
  // Load credentials
@@ -101,11 +100,11 @@ Output release details as JSON
101
100
  }
102
101
  else {
103
102
  this.error(`Workspace ID is required. Either:\n` +
104
- ` 1. Provide it as a flag: xano release push <directory> -n <name> -w <workspace_id>\n` +
103
+ ` 1. Provide it as a flag: xano release push -n <name> -w <workspace_id>\n` +
105
104
  ` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
106
105
  }
107
106
  // Resolve the input directory
108
- const inputDir = path.resolve(args.directory);
107
+ const inputDir = path.resolve(flags.directory);
109
108
  if (!fs.existsSync(inputDir)) {
110
109
  this.error(`Directory not found: ${inputDir}`);
111
110
  }
@@ -115,7 +114,7 @@ Output release details as JSON
115
114
  // Collect all .xs files from the directory tree
116
115
  const files = this.collectFiles(inputDir);
117
116
  if (files.length === 0) {
118
- this.error(`No .xs files found in ${args.directory}`);
117
+ this.error(`No .xs files found in ${flags.directory}`);
119
118
  }
120
119
  // Read each file and track file path alongside content
121
120
  const documentEntries = [];
@@ -126,7 +125,7 @@ Output release details as JSON
126
125
  }
127
126
  }
128
127
  if (documentEntries.length === 0) {
129
- this.error(`All .xs files in ${args.directory} are empty`);
128
+ this.error(`All .xs files in ${flags.directory} are empty`);
130
129
  }
131
130
  const multidoc = documentEntries.map((d) => d.content).join('\n---\n');
132
131
  // Construct the API URL with query params
@@ -1,11 +1,9 @@
1
1
  import BaseCommand from '../../../base-command.js';
2
2
  export default class SandboxPull 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,25 +1,28 @@
1
- import { Args, Flags } from '@oclif/core';
1
+ import { Flags } from '@oclif/core';
2
2
  import snakeCase from 'lodash.snakecase';
3
3
  import BaseCommand from '../../../base-command.js';
4
4
  import { buildApiGroupFolderResolver, parseDocument } from '../../../utils/document-parser.js';
5
5
  import * as fs from 'node:fs';
6
6
  import * as path from 'node:path';
7
7
  export default class SandboxPull extends BaseCommand {
8
- static args = {
9
- directory: Args.string({
10
- description: 'Output directory for pulled documents',
11
- required: true,
12
- }),
13
- };
14
8
  static description = 'Pull documents from your sandbox environment and split into individual files';
15
9
  static examples = [
16
- `$ xano sandbox pull ./my-sandbox
10
+ `$ xano sandbox pull
11
+ Pulled 42 documents from sandbox environment to current directory
12
+ `,
13
+ `$ xano sandbox pull -d ./my-sandbox
17
14
  Pulled 42 documents from sandbox environment to ./my-sandbox
18
15
  `,
19
- `$ xano sandbox pull ./backup --env --records`,
16
+ `$ xano sandbox pull --env --records`,
20
17
  ];
21
18
  static flags = {
22
19
  ...BaseCommand.baseFlags,
20
+ directory: Flags.string({
21
+ char: 'd',
22
+ default: '.',
23
+ description: 'Output directory for pulled documents (defaults to current directory)',
24
+ required: false,
25
+ }),
23
26
  draft: Flags.boolean({
24
27
  default: false,
25
28
  description: 'Include draft versions',
@@ -37,7 +40,7 @@ Pulled 42 documents from sandbox environment to ./my-sandbox
37
40
  }),
38
41
  };
39
42
  async run() {
40
- const { args, flags } = await this.parse(SandboxPull);
43
+ const { flags } = await this.parse(SandboxPull);
41
44
  const { profile } = this.resolveProfile(flags);
42
45
  const queryParams = new URLSearchParams({
43
46
  env: flags.env.toString(),
@@ -86,7 +89,7 @@ Pulled 42 documents from sandbox environment to ./my-sandbox
86
89
  this.log('No documents found in response');
87
90
  return;
88
91
  }
89
- const outputDir = path.resolve(args.directory);
92
+ const outputDir = path.resolve(flags.directory);
90
93
  fs.mkdirSync(outputDir, { recursive: true });
91
94
  const getApiGroupFolder = buildApiGroupFolderResolver(documents, snakeCase);
92
95
  const filenameCounters = new Map();
@@ -174,7 +177,7 @@ Pulled 42 documents from sandbox environment to ./my-sandbox
174
177
  fs.writeFileSync(filePath, doc.content, 'utf8');
175
178
  writtenCount++;
176
179
  }
177
- this.log(`Pulled ${writtenCount} documents from sandbox environment to ${args.directory}`);
180
+ this.log(`Pulled ${writtenCount} documents from sandbox environment to ${flags.directory}`);
178
181
  }
179
182
  sanitizeFilename(name) {
180
183
  return snakeCase(name.replaceAll('"', ''));
@@ -1,13 +1,19 @@
1
1
  import BaseCommand from '../../../base-command.js';
2
2
  export default class SandboxPush 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>;
7
+ delete: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
9
  env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ exclude: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ guids: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ include: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
14
  records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ review: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ sync: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
17
  transaction: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
18
  truncate: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
19
  config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -15,7 +21,6 @@ export default class SandboxPush extends BaseCommand {
15
21
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
22
  };
17
23
  run(): Promise<void>;
18
- private collectFiles;
19
- private renderBadIndexes;
20
- private renderBadReferences;
24
+ private openReview;
25
+ private getFrontendUrl;
21
26
  }
@@ -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,81 @@ 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
+ label: 'sandbox environment',
131
+ supportsBranches: false,
132
+ supportsPartial: false,
133
+ };
134
+ const pushFlags = {
135
+ delete: flags.delete,
136
+ 'dry-run': flags['dry-run'],
137
+ env: flags.env,
138
+ exclude: flags.exclude,
139
+ force: flags.force,
140
+ guids: flags.guids,
141
+ include: flags.include,
142
+ records: flags.records,
143
+ sync: flags.sync,
144
+ transaction: flags.transaction,
145
+ truncate: flags.truncate,
146
+ verbose: flags.verbose,
147
+ };
148
+ await executePush({
149
+ accessToken: profile.access_token,
150
+ branch: '',
151
+ command: this,
152
+ inputDir,
153
+ verboseFetch: this.verboseFetch.bind(this),
154
+ }, target, pushFlags);
155
+ if (flags.review) {
156
+ await this.openReview(profile.instance_origin, profile.access_token, flags.verbose);
69
157
  }
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);
158
+ }
159
+ async openReview(instanceOrigin, accessToken, verbose) {
160
+ const response = await this.verboseFetch(`${instanceOrigin}/api:meta/sandbox/impersonate`, {
161
+ headers: {
162
+ accept: 'application/json',
163
+ Authorization: `Bearer ${accessToken}`,
164
+ },
165
+ method: 'GET',
166
+ }, verbose, accessToken);
167
+ if (!response.ok) {
168
+ const message = await this.parseApiError(response, 'Failed to open sandbox review');
169
+ this.error(message);
74
170
  }
75
- // Check for indexes referencing non-existent schema fields
76
- const badIndexes = checkTableIndexes(documentEntries);
77
- if (badIndexes.length > 0) {
78
- this.renderBadIndexes(badIndexes);
171
+ const result = (await response.json());
172
+ if (!result._ti) {
173
+ this.error('No one-time token returned from impersonate API');
79
174
  }
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`);
175
+ const frontendUrl = this.getFrontendUrl(instanceOrigin);
176
+ const params = new URLSearchParams({ _ti: result._ti });
177
+ const reviewUrl = `${frontendUrl}/impersonate?${params.toString()}`;
178
+ this.log('Opening sandbox review...');
179
+ await open(reviewUrl);
143
180
  }
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);
181
+ getFrontendUrl(instanceOrigin) {
182
+ try {
183
+ const url = new URL(instanceOrigin);
184
+ if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
185
+ url.port = '4200';
186
+ return url.origin;
154
187
  }
155
188
  }
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`)}`);
189
+ catch {
190
+ // fall through
180
191
  }
181
- this.log('');
192
+ return instanceOrigin;
182
193
  }
183
194
  }
@@ -8,7 +8,6 @@ export default class TenantDeployRelease extends BaseCommand {
8
8
  static flags: {
9
9
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
10
  release: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
- transaction: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
11
  workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
12
  config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
13
  profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;