@xano/cli 0.0.14 → 0.0.16
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 +115 -14
- package/dist/commands/profile/create/index.d.ts +2 -0
- package/dist/commands/profile/create/index.js +15 -0
- package/dist/commands/profile/edit/index.d.ts +6 -0
- package/dist/commands/profile/edit/index.js +50 -1
- package/dist/commands/profile/list/index.js +5 -0
- package/dist/commands/profile/project/index.d.ts +6 -0
- package/dist/commands/profile/project/index.js +54 -0
- package/dist/commands/profile/token/index.d.ts +6 -0
- package/dist/commands/profile/token/index.js +54 -0
- package/dist/commands/profile/wizard/index.d.ts +2 -0
- package/dist/commands/profile/wizard/index.js +108 -0
- package/dist/commands/run/env/delete/index.d.ts +13 -0
- package/dist/commands/run/env/delete/index.js +65 -0
- package/dist/commands/run/env/get/index.d.ts +13 -0
- package/dist/commands/run/env/get/index.js +52 -0
- package/dist/commands/run/env/list/index.d.ts +11 -0
- package/dist/commands/run/env/list/index.js +58 -0
- package/dist/commands/run/env/set/index.d.ts +13 -0
- package/dist/commands/run/env/set/index.js +51 -0
- package/dist/commands/{ephemeral/run/job → run/exec}/index.d.ts +4 -3
- package/dist/commands/run/exec/index.js +353 -0
- package/dist/commands/{ephemeral/run/service → run/info}/index.d.ts +3 -5
- package/dist/commands/run/info/index.js +160 -0
- package/dist/commands/run/projects/create/index.d.ts +13 -0
- package/dist/commands/run/projects/create/index.js +75 -0
- package/dist/commands/run/projects/delete/index.d.ts +13 -0
- package/dist/commands/run/projects/delete/index.js +65 -0
- package/dist/commands/run/projects/list/index.d.ts +12 -0
- package/dist/commands/run/projects/list/index.js +66 -0
- package/dist/commands/run/projects/update/index.d.ts +15 -0
- package/dist/commands/run/projects/update/index.js +86 -0
- package/dist/commands/run/secrets/delete/index.d.ts +13 -0
- package/dist/commands/run/secrets/delete/index.js +65 -0
- package/dist/commands/run/secrets/get/index.d.ts +13 -0
- package/dist/commands/run/secrets/get/index.js +52 -0
- package/dist/commands/run/secrets/list/index.d.ts +11 -0
- package/dist/commands/run/secrets/list/index.js +62 -0
- package/dist/commands/run/secrets/set/index.d.ts +15 -0
- package/dist/commands/run/secrets/set/index.js +74 -0
- package/dist/commands/run/sessions/delete/index.d.ts +13 -0
- package/dist/commands/run/sessions/delete/index.js +65 -0
- package/dist/commands/run/sessions/get/index.d.ts +13 -0
- package/dist/commands/run/sessions/get/index.js +72 -0
- package/dist/commands/run/sessions/list/index.d.ts +12 -0
- package/dist/commands/run/sessions/list/index.js +64 -0
- package/dist/commands/run/sessions/start/index.d.ts +13 -0
- package/dist/commands/run/sessions/start/index.js +56 -0
- package/dist/commands/run/sessions/stop/index.d.ts +13 -0
- package/dist/commands/run/sessions/stop/index.js +56 -0
- package/dist/commands/run/sink/get/index.d.ts +13 -0
- package/dist/commands/run/sink/get/index.js +63 -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.d.ts +42 -0
- package/dist/lib/base-run-command.js +75 -0
- package/dist/lib/run-http-client.d.ts +58 -0
- package/dist/lib/run-http-client.js +136 -0
- package/dist/lib/run-types.d.ts +226 -0
- package/dist/lib/run-types.js +5 -0
- package/oclif.manifest.json +1470 -218
- package/package.json +1 -1
- package/dist/commands/ephemeral/run/job/index.js +0 -311
- package/dist/commands/ephemeral/run/service/index.js +0 -287
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base command for all run commands
|
|
3
|
+
*/
|
|
4
|
+
import BaseCommand from '../base-command.js';
|
|
5
|
+
import { RunHttpClient } from './run-http-client.js';
|
|
6
|
+
export interface ProfileConfig {
|
|
7
|
+
account_origin?: string;
|
|
8
|
+
instance_origin: string;
|
|
9
|
+
access_token: string;
|
|
10
|
+
workspace?: string;
|
|
11
|
+
branch?: string;
|
|
12
|
+
project?: string;
|
|
13
|
+
run_project?: string;
|
|
14
|
+
run_base_url?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface CredentialsFile {
|
|
17
|
+
profiles: {
|
|
18
|
+
[key: string]: ProfileConfig;
|
|
19
|
+
};
|
|
20
|
+
default?: string;
|
|
21
|
+
}
|
|
22
|
+
export default abstract class BaseRunCommand extends BaseCommand {
|
|
23
|
+
protected httpClient: RunHttpClient;
|
|
24
|
+
protected profile: ProfileConfig;
|
|
25
|
+
protected profileName: string;
|
|
26
|
+
/**
|
|
27
|
+
* Initialize the run command with profile and HTTP client
|
|
28
|
+
*/
|
|
29
|
+
protected initRunCommand(profileFlag?: string): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Initialize with project required
|
|
32
|
+
*/
|
|
33
|
+
protected initRunCommandWithProject(profileFlag?: string): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Load credentials from file
|
|
36
|
+
*/
|
|
37
|
+
protected loadCredentials(): CredentialsFile;
|
|
38
|
+
/**
|
|
39
|
+
* Format a response for JSON output
|
|
40
|
+
*/
|
|
41
|
+
protected outputJson(data: unknown): void;
|
|
42
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base command for all run commands
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from 'node:fs';
|
|
5
|
+
import * as os from 'node:os';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import * as yaml from 'js-yaml';
|
|
8
|
+
import BaseCommand from '../base-command.js';
|
|
9
|
+
import { DEFAULT_RUN_BASE_URL, RunHttpClient } from './run-http-client.js';
|
|
10
|
+
export default class BaseRunCommand extends BaseCommand {
|
|
11
|
+
httpClient;
|
|
12
|
+
profile;
|
|
13
|
+
profileName;
|
|
14
|
+
/**
|
|
15
|
+
* Initialize the run command with profile and HTTP client
|
|
16
|
+
*/
|
|
17
|
+
async initRunCommand(profileFlag) {
|
|
18
|
+
this.profileName = profileFlag || this.getDefaultProfile();
|
|
19
|
+
const credentials = this.loadCredentials();
|
|
20
|
+
if (!(this.profileName in credentials.profiles)) {
|
|
21
|
+
this.error(`Profile '${this.profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
22
|
+
`Create a profile using 'xano profile:create'`);
|
|
23
|
+
}
|
|
24
|
+
this.profile = credentials.profiles[this.profileName];
|
|
25
|
+
if (!this.profile.access_token) {
|
|
26
|
+
this.error(`Profile '${this.profileName}' is missing access_token`);
|
|
27
|
+
}
|
|
28
|
+
const baseUrl = this.profile.run_base_url || DEFAULT_RUN_BASE_URL;
|
|
29
|
+
// Use run_project if available, fall back to project for backward compatibility
|
|
30
|
+
const projectId = this.profile.run_project || this.profile.project;
|
|
31
|
+
this.httpClient = new RunHttpClient({
|
|
32
|
+
baseUrl,
|
|
33
|
+
authToken: this.profile.access_token,
|
|
34
|
+
projectId,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Initialize with project required
|
|
39
|
+
*/
|
|
40
|
+
async initRunCommandWithProject(profileFlag) {
|
|
41
|
+
await this.initRunCommand(profileFlag);
|
|
42
|
+
if (!this.profile.run_project && !this.profile.project) {
|
|
43
|
+
this.error(`Profile '${this.profileName}' is missing run_project. ` +
|
|
44
|
+
`Run 'xano profile:wizard' to set up your profile or use 'xano profile:edit --run-project <project-id>'`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Load credentials from file
|
|
49
|
+
*/
|
|
50
|
+
loadCredentials() {
|
|
51
|
+
const configDir = path.join(os.homedir(), '.xano');
|
|
52
|
+
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
53
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
54
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
55
|
+
`Create a profile using 'xano profile:create'`);
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
59
|
+
const parsed = yaml.load(fileContent);
|
|
60
|
+
if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
|
|
61
|
+
this.error('Credentials file has invalid format.');
|
|
62
|
+
}
|
|
63
|
+
return parsed;
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
this.error(`Failed to parse credentials file: ${error}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Format a response for JSON output
|
|
71
|
+
*/
|
|
72
|
+
outputJson(data) {
|
|
73
|
+
this.log(JSON.stringify(data, null, 2));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for Xano Run API
|
|
3
|
+
* Based on @xano/run-sdk HttpClient
|
|
4
|
+
*/
|
|
5
|
+
export declare const DEFAULT_RUN_BASE_URL = "https://app.xano.com/";
|
|
6
|
+
export interface RunHttpClientConfig {
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
authToken: string;
|
|
9
|
+
projectId?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class RunHttpClient {
|
|
12
|
+
private readonly config;
|
|
13
|
+
constructor(config: RunHttpClientConfig);
|
|
14
|
+
/**
|
|
15
|
+
* Get the project ID
|
|
16
|
+
*/
|
|
17
|
+
getProjectId(): string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Build headers for a request
|
|
20
|
+
*/
|
|
21
|
+
getHeaders(contentType?: string): HeadersInit;
|
|
22
|
+
/**
|
|
23
|
+
* Build a URL with optional query parameters
|
|
24
|
+
*/
|
|
25
|
+
buildUrl(path: string, queryParams?: Record<string, unknown>): string;
|
|
26
|
+
/**
|
|
27
|
+
* Build a URL scoped to the current project
|
|
28
|
+
*/
|
|
29
|
+
buildProjectUrl(path: string, queryParams?: Record<string, unknown>): string;
|
|
30
|
+
/**
|
|
31
|
+
* Build a URL scoped to a specific session
|
|
32
|
+
*/
|
|
33
|
+
buildSessionUrl(sessionId: string, path?: string, queryParams?: Record<string, unknown>): string;
|
|
34
|
+
/**
|
|
35
|
+
* Make an HTTP request
|
|
36
|
+
*/
|
|
37
|
+
request<T>(url: string, options: RequestInit): Promise<T>;
|
|
38
|
+
/**
|
|
39
|
+
* Make a GET request
|
|
40
|
+
*/
|
|
41
|
+
get<T>(url: string): Promise<T>;
|
|
42
|
+
/**
|
|
43
|
+
* Make a POST request with JSON body
|
|
44
|
+
*/
|
|
45
|
+
post<T>(url: string, body?: unknown): Promise<T>;
|
|
46
|
+
/**
|
|
47
|
+
* Make a POST request with XanoScript body
|
|
48
|
+
*/
|
|
49
|
+
postXanoScript<T>(url: string, code: string): Promise<T>;
|
|
50
|
+
/**
|
|
51
|
+
* Make a PATCH request
|
|
52
|
+
*/
|
|
53
|
+
patch<T>(url: string, body: unknown): Promise<T>;
|
|
54
|
+
/**
|
|
55
|
+
* Make a DELETE request
|
|
56
|
+
*/
|
|
57
|
+
delete<T>(url: string, body?: unknown): Promise<T>;
|
|
58
|
+
}
|