@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.
- package/README.md +78 -53
- package/dist/commands/release/pull/index.d.ts +1 -3
- package/dist/commands/release/pull/index.js +18 -15
- package/dist/commands/release/push/index.d.ts +1 -3
- package/dist/commands/release/push/index.js +18 -19
- package/dist/commands/sandbox/pull/index.d.ts +1 -3
- package/dist/commands/sandbox/pull/index.js +15 -12
- package/dist/commands/sandbox/push/index.d.ts +11 -6
- package/dist/commands/sandbox/push/index.js +149 -136
- package/dist/commands/tenant/pull/index.d.ts +1 -3
- package/dist/commands/tenant/pull/index.js +19 -18
- package/dist/commands/workspace/git/pull/index.d.ts +1 -3
- package/dist/commands/workspace/git/pull/index.js +18 -17
- package/dist/commands/workspace/pull/index.d.ts +1 -3
- package/dist/commands/workspace/pull/index.js +20 -21
- package/dist/commands/workspace/push/index.d.ts +6 -18
- package/dist/commands/workspace/push/index.js +85 -747
- package/dist/utils/multidoc-push.d.ts +63 -0
- package/dist/utils/multidoc-push.js +674 -0
- package/dist/utils/reference-checker.js +2 -2
- package/oclif.manifest.json +2121 -2015
- package/package.json +1 -1
|
@@ -1,36 +1,106 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
|
-
import
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import open from 'open';
|
|
4
5
|
import BaseCommand from '../../../base-command.js';
|
|
5
|
-
import {
|
|
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
|
|
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
|
|
17
|
-
|
|
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 {
|
|
117
|
+
const { flags } = await this.parse(SandboxPush);
|
|
48
118
|
const { profile } = this.resolveProfile(flags);
|
|
49
|
-
const inputDir =
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
24
|
-
Pulled 58 documents from tenant my-tenant
|
|
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 {
|
|
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
|
|
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(
|
|
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 ${
|
|
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 {
|
|
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
|
|
19
|
-
`$ xano workspace git pull ./output -r https://github.com/owner/repo
|
|
20
|
-
`$ xano workspace git pull
|
|
21
|
-
`$ xano workspace git pull
|
|
22
|
-
`$ xano workspace git pull
|
|
23
|
-
`$ xano workspace git pull
|
|
24
|
-
`$ xano workspace git pull
|
|
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 {
|
|
51
|
+
const { flags } = await this.parse(GitPull);
|
|
51
52
|
const token = flags.token || '';
|
|
52
|
-
const outputDir = path.resolve(
|
|
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 ${
|
|
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 {
|
|
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
|
|
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
|
|
24
|
-
Pulled 58 documents
|
|
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 {
|
|
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
|
|
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(
|
|
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 ${
|
|
254
|
+
this.log(`Pulled ${writtenCount} documents to ${flags.directory}`);
|
|
256
255
|
}
|
|
257
256
|
loadCredentials() {
|
|
258
257
|
const credentialsPath = this.getCredentialsPath();
|