@xano/cli 0.0.15 → 0.0.17
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 +20 -4
- package/dist/commands/profile/edit/index.js +1 -1
- package/dist/commands/profile/wizard/index.d.ts +1 -0
- package/dist/commands/profile/wizard/index.js +38 -0
- package/dist/commands/workspace/pull/index.d.ts +28 -0
- package/dist/commands/workspace/pull/index.js +238 -0
- package/dist/commands/workspace/push/index.d.ts +19 -0
- package/dist/commands/workspace/push/index.js +163 -0
- package/dist/lib/base-run-command.js +1 -1
- package/oclif.manifest.json +509 -389
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ npm install -g @xano/cli
|
|
|
35
35
|
Profiles store your Xano credentials and default workspace/project settings.
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
# Create a profile interactively
|
|
38
|
+
# Create a profile interactively (auto-fetches run projects)
|
|
39
39
|
xano profile:wizard
|
|
40
40
|
|
|
41
41
|
# Create a profile manually
|
|
@@ -52,13 +52,17 @@ xano profile:list --details
|
|
|
52
52
|
xano profile:set-default myprofile
|
|
53
53
|
|
|
54
54
|
# Edit a profile
|
|
55
|
-
xano profile:edit myprofile -w 123
|
|
56
|
-
xano profile:edit myprofile -j my-project
|
|
55
|
+
xano profile:edit myprofile -w 123 # Set default workspace
|
|
56
|
+
xano profile:edit myprofile -j my-project # Set default project
|
|
57
|
+
xano profile:edit myprofile --run-project <id> # Set run project for xano run commands
|
|
58
|
+
xano profile:edit myprofile --remove-run-project # Remove run project
|
|
57
59
|
|
|
58
60
|
# Delete a profile
|
|
59
61
|
xano profile:delete myprofile
|
|
60
62
|
```
|
|
61
63
|
|
|
64
|
+
The `profile:wizard` command automatically fetches your run projects and sets the first one as the default for `xano run` commands.
|
|
65
|
+
|
|
62
66
|
### Workspaces
|
|
63
67
|
|
|
64
68
|
```bash
|
|
@@ -211,7 +215,19 @@ All commands support these options:
|
|
|
211
215
|
|
|
212
216
|
## Configuration
|
|
213
217
|
|
|
214
|
-
Profiles are stored in `~/.xano/credentials.yaml
|
|
218
|
+
Profiles are stored in `~/.xano/credentials.yaml`:
|
|
219
|
+
|
|
220
|
+
```yaml
|
|
221
|
+
profiles:
|
|
222
|
+
default:
|
|
223
|
+
account_origin: https://app.xano.com
|
|
224
|
+
instance_origin: https://instance.xano.com
|
|
225
|
+
access_token: <token>
|
|
226
|
+
workspace: <workspace_id>
|
|
227
|
+
branch: <branch_id>
|
|
228
|
+
run_project: <run_project_id> # Used by xano run commands
|
|
229
|
+
default: default
|
|
230
|
+
```
|
|
215
231
|
|
|
216
232
|
## Help
|
|
217
233
|
|
|
@@ -4,6 +4,7 @@ import * as os from 'node:os';
|
|
|
4
4
|
import * as path from 'node:path';
|
|
5
5
|
import * as yaml from 'js-yaml';
|
|
6
6
|
import inquirer from 'inquirer';
|
|
7
|
+
import { DEFAULT_RUN_BASE_URL } from '../../../lib/run-http-client.js';
|
|
7
8
|
export default class ProfileWizard extends Command {
|
|
8
9
|
static flags = {
|
|
9
10
|
name: Flags.string({
|
|
@@ -191,6 +192,25 @@ Profile 'production' created successfully at ~/.xano/credentials.yaml
|
|
|
191
192
|
}
|
|
192
193
|
}
|
|
193
194
|
}
|
|
195
|
+
// Step 7: Fetch run projects and auto-select the first one if no project was selected
|
|
196
|
+
this.log('');
|
|
197
|
+
this.log('Fetching available run projects...');
|
|
198
|
+
try {
|
|
199
|
+
const runProjects = await this.fetchRunProjects(accessToken);
|
|
200
|
+
if (runProjects.length > 0) {
|
|
201
|
+
// Use run project if no metadata project was selected
|
|
202
|
+
if (!project) {
|
|
203
|
+
project = runProjects[0].id;
|
|
204
|
+
}
|
|
205
|
+
this.log(`✓ Found ${runProjects.length} run project(s). Using "${runProjects[0].name}" as default.`);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
this.log('No run projects found. You can create one later with "xano run projects create".');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// Silently ignore - project will remain undefined
|
|
213
|
+
}
|
|
194
214
|
// Save profile
|
|
195
215
|
await this.saveProfile({
|
|
196
216
|
name: profileName,
|
|
@@ -357,6 +377,24 @@ Profile 'production' created successfully at ~/.xano/credentials.yaml
|
|
|
357
377
|
}
|
|
358
378
|
return [];
|
|
359
379
|
}
|
|
380
|
+
async fetchRunProjects(accessToken, runBaseUrl = DEFAULT_RUN_BASE_URL) {
|
|
381
|
+
const baseUrl = runBaseUrl.endsWith('/') ? runBaseUrl.slice(0, -1) : runBaseUrl;
|
|
382
|
+
const response = await fetch(`${baseUrl}/api:run/project`, {
|
|
383
|
+
method: 'GET',
|
|
384
|
+
headers: {
|
|
385
|
+
'Content-Type': 'application/json',
|
|
386
|
+
Authorization: `Bearer ${accessToken}`,
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
if (!response.ok) {
|
|
390
|
+
if (response.status === 401) {
|
|
391
|
+
throw new Error('Unauthorized. Please check your access token.');
|
|
392
|
+
}
|
|
393
|
+
throw new Error(`API request failed with status ${response.status}`);
|
|
394
|
+
}
|
|
395
|
+
const data = (await response.json());
|
|
396
|
+
return Array.isArray(data) ? data : [];
|
|
397
|
+
}
|
|
360
398
|
getDefaultProfileName() {
|
|
361
399
|
try {
|
|
362
400
|
const configDir = path.join(os.homedir(), '.xano');
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export default class Pull extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
directory: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static flags: {
|
|
7
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
};
|
|
12
|
+
static description: string;
|
|
13
|
+
static examples: string[];
|
|
14
|
+
run(): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Parse a single document to extract its type, name, and optional verb.
|
|
17
|
+
* Skips leading comment lines (starting with //) to find the first
|
|
18
|
+
* meaningful line containing the type keyword and name.
|
|
19
|
+
*/
|
|
20
|
+
private parseDocument;
|
|
21
|
+
/**
|
|
22
|
+
* Sanitize a document name for use as a filename.
|
|
23
|
+
* Strips quotes, replaces spaces with underscores, and removes
|
|
24
|
+
* characters that are unsafe in filenames.
|
|
25
|
+
*/
|
|
26
|
+
private sanitizeFilename;
|
|
27
|
+
private loadCredentials;
|
|
28
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import * as yaml from 'js-yaml';
|
|
6
|
+
import BaseCommand from '../../../base-command.js';
|
|
7
|
+
export default class Pull extends BaseCommand {
|
|
8
|
+
static args = {
|
|
9
|
+
directory: Args.string({
|
|
10
|
+
description: 'Output directory for pulled documents',
|
|
11
|
+
required: true,
|
|
12
|
+
}),
|
|
13
|
+
};
|
|
14
|
+
static flags = {
|
|
15
|
+
...BaseCommand.baseFlags,
|
|
16
|
+
workspace: Flags.string({
|
|
17
|
+
char: 'w',
|
|
18
|
+
description: 'Workspace ID (optional if set in profile)',
|
|
19
|
+
required: false,
|
|
20
|
+
}),
|
|
21
|
+
env: Flags.boolean({
|
|
22
|
+
description: 'Include environment variables',
|
|
23
|
+
required: false,
|
|
24
|
+
default: false,
|
|
25
|
+
}),
|
|
26
|
+
records: Flags.boolean({
|
|
27
|
+
description: 'Include records',
|
|
28
|
+
required: false,
|
|
29
|
+
default: false,
|
|
30
|
+
}),
|
|
31
|
+
};
|
|
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
|
+
async run() {
|
|
45
|
+
const { args, flags } = await this.parse(Pull);
|
|
46
|
+
// Get profile name (default or from flag/env)
|
|
47
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
48
|
+
// Load credentials
|
|
49
|
+
const credentials = this.loadCredentials();
|
|
50
|
+
// Get the profile configuration
|
|
51
|
+
if (!(profileName in credentials.profiles)) {
|
|
52
|
+
this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
53
|
+
`Create a profile using 'xano profile:create'`);
|
|
54
|
+
}
|
|
55
|
+
const profile = credentials.profiles[profileName];
|
|
56
|
+
// Validate required fields
|
|
57
|
+
if (!profile.instance_origin) {
|
|
58
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
59
|
+
}
|
|
60
|
+
if (!profile.access_token) {
|
|
61
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
62
|
+
}
|
|
63
|
+
// Determine workspace_id from flag or profile
|
|
64
|
+
let workspaceId;
|
|
65
|
+
if (flags.workspace) {
|
|
66
|
+
workspaceId = flags.workspace;
|
|
67
|
+
}
|
|
68
|
+
else if (profile.workspace) {
|
|
69
|
+
workspaceId = profile.workspace;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
this.error(`Workspace ID is required. Either:\n` +
|
|
73
|
+
` 1. Provide it as a flag: xano workspace pull <directory> -w <workspace_id>\n` +
|
|
74
|
+
` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
|
|
75
|
+
}
|
|
76
|
+
// Build query parameters
|
|
77
|
+
const queryParams = new URLSearchParams({
|
|
78
|
+
env: flags.env.toString(),
|
|
79
|
+
records: flags.records.toString(),
|
|
80
|
+
});
|
|
81
|
+
// Construct the API URL
|
|
82
|
+
const apiUrl = `${profile.instance_origin}/api:meta/beta/workspace/${workspaceId}/multidoc?${queryParams.toString()}`;
|
|
83
|
+
// Fetch multidoc from the API
|
|
84
|
+
let responseText;
|
|
85
|
+
try {
|
|
86
|
+
const response = await fetch(apiUrl, {
|
|
87
|
+
method: 'GET',
|
|
88
|
+
headers: {
|
|
89
|
+
'accept': 'application/json',
|
|
90
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
const errorText = await response.text();
|
|
95
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
96
|
+
}
|
|
97
|
+
responseText = await response.text();
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (error instanceof Error) {
|
|
101
|
+
this.error(`Failed to fetch multidoc: ${error.message}`);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
this.error(`Failed to fetch multidoc: ${String(error)}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Split the response into individual documents
|
|
108
|
+
const rawDocuments = responseText.split('\n---\n');
|
|
109
|
+
// Parse each document
|
|
110
|
+
const documents = [];
|
|
111
|
+
for (const raw of rawDocuments) {
|
|
112
|
+
const trimmed = raw.trim();
|
|
113
|
+
if (!trimmed) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const parsed = this.parseDocument(trimmed);
|
|
117
|
+
if (parsed) {
|
|
118
|
+
documents.push(parsed);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (documents.length === 0) {
|
|
122
|
+
this.log('No documents found in response');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// Resolve the output directory
|
|
126
|
+
const outputDir = path.resolve(args.directory);
|
|
127
|
+
// Create the output directory if it doesn't exist
|
|
128
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
129
|
+
// Track filenames per type to handle duplicates
|
|
130
|
+
const filenameCounters = new Map();
|
|
131
|
+
let writtenCount = 0;
|
|
132
|
+
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}`;
|
|
140
|
+
}
|
|
141
|
+
// Track duplicates per type directory
|
|
142
|
+
if (!filenameCounters.has(doc.type)) {
|
|
143
|
+
filenameCounters.set(doc.type, new Map());
|
|
144
|
+
}
|
|
145
|
+
const typeCounters = filenameCounters.get(doc.type);
|
|
146
|
+
const count = typeCounters.get(baseName) || 0;
|
|
147
|
+
typeCounters.set(baseName, count + 1);
|
|
148
|
+
// Append numeric suffix for duplicates
|
|
149
|
+
let filename;
|
|
150
|
+
if (count === 0) {
|
|
151
|
+
filename = `${baseName}.xs`;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
filename = `${baseName}_${count + 1}.xs`;
|
|
155
|
+
}
|
|
156
|
+
const filePath = path.join(typeDir, filename);
|
|
157
|
+
fs.writeFileSync(filePath, doc.content, 'utf8');
|
|
158
|
+
writtenCount++;
|
|
159
|
+
}
|
|
160
|
+
this.log(`Pulled ${writtenCount} documents to ${args.directory}`);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Parse a single document to extract its type, name, and optional verb.
|
|
164
|
+
* Skips leading comment lines (starting with //) to find the first
|
|
165
|
+
* meaningful line containing the type keyword and name.
|
|
166
|
+
*/
|
|
167
|
+
parseDocument(content) {
|
|
168
|
+
const lines = content.split('\n');
|
|
169
|
+
// Find the first non-comment line
|
|
170
|
+
let firstLine = null;
|
|
171
|
+
for (const line of lines) {
|
|
172
|
+
const trimmedLine = line.trim();
|
|
173
|
+
if (trimmedLine && !trimmedLine.startsWith('//')) {
|
|
174
|
+
firstLine = trimmedLine;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (!firstLine) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
// Parse the type keyword and name from the first meaningful line
|
|
182
|
+
// Expected formats:
|
|
183
|
+
// type name {
|
|
184
|
+
// type name verb=GET {
|
|
185
|
+
// type "name with spaces" {
|
|
186
|
+
// type "name with spaces" verb=PATCH {
|
|
187
|
+
const match = firstLine.match(/^(\w+)\s+("(?:[^"\\]|\\.)*"|\S+)(?:\s+(.*))?/);
|
|
188
|
+
if (!match) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
const type = match[1];
|
|
192
|
+
let name = match[2];
|
|
193
|
+
const rest = match[3] || '';
|
|
194
|
+
// Strip surrounding quotes from the name
|
|
195
|
+
if (name.startsWith('"') && name.endsWith('"')) {
|
|
196
|
+
name = name.slice(1, -1);
|
|
197
|
+
}
|
|
198
|
+
// Extract verb if present (e.g., verb=GET)
|
|
199
|
+
let verb;
|
|
200
|
+
const verbMatch = rest.match(/verb=(\S+)/);
|
|
201
|
+
if (verbMatch) {
|
|
202
|
+
verb = verbMatch[1];
|
|
203
|
+
}
|
|
204
|
+
return { type, name, verb, content };
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Sanitize a document name for use as a filename.
|
|
208
|
+
* Strips quotes, replaces spaces with underscores, and removes
|
|
209
|
+
* characters that are unsafe in filenames.
|
|
210
|
+
*/
|
|
211
|
+
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
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export default class Push extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
directory: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
};
|
|
12
|
+
run(): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Recursively collect all .xs files from a directory, sorted by
|
|
15
|
+
* type subdirectory name then filename for deterministic ordering.
|
|
16
|
+
*/
|
|
17
|
+
private collectFiles;
|
|
18
|
+
private loadCredentials;
|
|
19
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import * as yaml from 'js-yaml';
|
|
6
|
+
import BaseCommand from '../../../base-command.js';
|
|
7
|
+
export default class Push extends BaseCommand {
|
|
8
|
+
static args = {
|
|
9
|
+
directory: Args.string({
|
|
10
|
+
description: 'Directory containing documents to push (as produced by workspace pull)',
|
|
11
|
+
required: true,
|
|
12
|
+
}),
|
|
13
|
+
};
|
|
14
|
+
static description = 'Push local documents to a workspace via the Xano Metadata API multidoc endpoint';
|
|
15
|
+
static examples = [
|
|
16
|
+
`$ xano workspace push ./my-workspace
|
|
17
|
+
Pushed 42 documents from ./my-workspace
|
|
18
|
+
`,
|
|
19
|
+
`$ xano workspace push ./output -w 40
|
|
20
|
+
Pushed 15 documents from ./output
|
|
21
|
+
`,
|
|
22
|
+
`$ xano workspace push ./backup --profile production
|
|
23
|
+
Pushed 58 documents from ./backup
|
|
24
|
+
`,
|
|
25
|
+
];
|
|
26
|
+
static flags = {
|
|
27
|
+
...BaseCommand.baseFlags,
|
|
28
|
+
workspace: Flags.string({
|
|
29
|
+
char: 'w',
|
|
30
|
+
description: 'Workspace ID (optional if set in profile)',
|
|
31
|
+
required: false,
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
async run() {
|
|
35
|
+
const { args, flags } = await this.parse(Push);
|
|
36
|
+
// Get profile name (default or from flag/env)
|
|
37
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
38
|
+
// Load credentials
|
|
39
|
+
const credentials = this.loadCredentials();
|
|
40
|
+
// Get the profile configuration
|
|
41
|
+
if (!(profileName in credentials.profiles)) {
|
|
42
|
+
this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
43
|
+
`Create a profile using 'xano profile:create'`);
|
|
44
|
+
}
|
|
45
|
+
const profile = credentials.profiles[profileName];
|
|
46
|
+
// Validate required fields
|
|
47
|
+
if (!profile.instance_origin) {
|
|
48
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
49
|
+
}
|
|
50
|
+
if (!profile.access_token) {
|
|
51
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
52
|
+
}
|
|
53
|
+
// Determine workspace_id from flag or profile
|
|
54
|
+
let workspaceId;
|
|
55
|
+
if (flags.workspace) {
|
|
56
|
+
workspaceId = flags.workspace;
|
|
57
|
+
}
|
|
58
|
+
else if (profile.workspace) {
|
|
59
|
+
workspaceId = profile.workspace;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
this.error(`Workspace ID is required. Either:\n` +
|
|
63
|
+
` 1. Provide it as a flag: xano workspace push <directory> -w <workspace_id>\n` +
|
|
64
|
+
` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
|
|
65
|
+
}
|
|
66
|
+
// Resolve the input directory
|
|
67
|
+
const inputDir = path.resolve(args.directory);
|
|
68
|
+
if (!fs.existsSync(inputDir)) {
|
|
69
|
+
this.error(`Directory not found: ${inputDir}`);
|
|
70
|
+
}
|
|
71
|
+
if (!fs.statSync(inputDir).isDirectory()) {
|
|
72
|
+
this.error(`Not a directory: ${inputDir}`);
|
|
73
|
+
}
|
|
74
|
+
// Collect all .xs files from the directory tree
|
|
75
|
+
const files = this.collectFiles(inputDir);
|
|
76
|
+
if (files.length === 0) {
|
|
77
|
+
this.error(`No .xs files found in ${args.directory}`);
|
|
78
|
+
}
|
|
79
|
+
// Read each file and join with --- separator
|
|
80
|
+
const documents = [];
|
|
81
|
+
for (const filePath of files) {
|
|
82
|
+
const content = fs.readFileSync(filePath, 'utf8').trim();
|
|
83
|
+
if (content) {
|
|
84
|
+
documents.push(content);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (documents.length === 0) {
|
|
88
|
+
this.error(`All .xs files in ${args.directory} are empty`);
|
|
89
|
+
}
|
|
90
|
+
const multidoc = documents.join('\n---\n');
|
|
91
|
+
// Construct the API URL
|
|
92
|
+
const apiUrl = `${profile.instance_origin}/api:meta/beta/workspace/${workspaceId}/multidoc`;
|
|
93
|
+
// POST the multidoc to the API
|
|
94
|
+
try {
|
|
95
|
+
const response = await fetch(apiUrl, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: {
|
|
98
|
+
'accept': 'application/json',
|
|
99
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
100
|
+
'Content-Type': 'text/x-xanoscript',
|
|
101
|
+
},
|
|
102
|
+
body: multidoc,
|
|
103
|
+
});
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
const errorText = await response.text();
|
|
106
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
107
|
+
}
|
|
108
|
+
// Log the response if any
|
|
109
|
+
const responseText = await response.text();
|
|
110
|
+
if (responseText) {
|
|
111
|
+
this.log(responseText);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (error instanceof Error) {
|
|
116
|
+
this.error(`Failed to push multidoc: ${error.message}`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
this.error(`Failed to push multidoc: ${String(error)}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
this.log(`Pushed ${documents.length} documents from ${args.directory}`);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Recursively collect all .xs files from a directory, sorted by
|
|
126
|
+
* type subdirectory name then filename for deterministic ordering.
|
|
127
|
+
*/
|
|
128
|
+
collectFiles(dir) {
|
|
129
|
+
const files = [];
|
|
130
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
const fullPath = path.join(dir, entry.name);
|
|
133
|
+
if (entry.isDirectory()) {
|
|
134
|
+
files.push(...this.collectFiles(fullPath));
|
|
135
|
+
}
|
|
136
|
+
else if (entry.isFile() && entry.name.endsWith('.xs')) {
|
|
137
|
+
files.push(fullPath);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return files.sort();
|
|
141
|
+
}
|
|
142
|
+
loadCredentials() {
|
|
143
|
+
const configDir = path.join(os.homedir(), '.xano');
|
|
144
|
+
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
145
|
+
// Check if credentials file exists
|
|
146
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
147
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
148
|
+
`Create a profile using 'xano profile:create'`);
|
|
149
|
+
}
|
|
150
|
+
// Read credentials file
|
|
151
|
+
try {
|
|
152
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
153
|
+
const parsed = yaml.load(fileContent);
|
|
154
|
+
if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
|
|
155
|
+
this.error('Credentials file has invalid format.');
|
|
156
|
+
}
|
|
157
|
+
return parsed;
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
this.error(`Failed to parse credentials file: ${error}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -39,7 +39,7 @@ export default class BaseRunCommand extends BaseCommand {
|
|
|
39
39
|
await this.initRunCommand(profileFlag);
|
|
40
40
|
if (!this.profile.project) {
|
|
41
41
|
this.error(`Profile '${this.profileName}' is missing project. ` +
|
|
42
|
-
`
|
|
42
|
+
`Run 'xano profile:wizard' to set up your profile or use 'xano profile:edit --project <project-id>'`);
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
/**
|