@xano/cli 0.0.2
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 +1200 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/dist/base-command.d.ts +11 -0
- package/dist/base-command.js +40 -0
- package/dist/commands/ephemeral/run/job/index.d.ts +19 -0
- package/dist/commands/ephemeral/run/job/index.js +318 -0
- package/dist/commands/ephemeral/run/service/index.d.ts +18 -0
- package/dist/commands/ephemeral/run/service/index.js +286 -0
- package/dist/commands/function/create/index.d.ts +18 -0
- package/dist/commands/function/create/index.js +280 -0
- package/dist/commands/function/edit/index.d.ts +24 -0
- package/dist/commands/function/edit/index.js +482 -0
- package/dist/commands/function/get/index.d.ts +18 -0
- package/dist/commands/function/get/index.js +279 -0
- package/dist/commands/function/list/index.d.ts +19 -0
- package/dist/commands/function/list/index.js +208 -0
- package/dist/commands/profile/create/index.d.ts +17 -0
- package/dist/commands/profile/create/index.js +123 -0
- package/dist/commands/profile/delete/index.d.ts +14 -0
- package/dist/commands/profile/delete/index.js +124 -0
- package/dist/commands/profile/edit/index.d.ts +18 -0
- package/dist/commands/profile/edit/index.js +129 -0
- package/dist/commands/profile/get-default/index.d.ts +6 -0
- package/dist/commands/profile/get-default/index.js +44 -0
- package/dist/commands/profile/list/index.d.ts +10 -0
- package/dist/commands/profile/list/index.js +115 -0
- package/dist/commands/profile/set-default/index.d.ts +9 -0
- package/dist/commands/profile/set-default/index.js +63 -0
- package/dist/commands/profile/wizard/index.d.ts +15 -0
- package/dist/commands/profile/wizard/index.js +350 -0
- package/dist/commands/static_host/build/create/index.d.ts +18 -0
- package/dist/commands/static_host/build/create/index.js +194 -0
- package/dist/commands/static_host/build/get/index.d.ts +16 -0
- package/dist/commands/static_host/build/get/index.js +165 -0
- package/dist/commands/static_host/build/list/index.d.ts +17 -0
- package/dist/commands/static_host/build/list/index.js +192 -0
- package/dist/commands/static_host/list/index.d.ts +15 -0
- package/dist/commands/static_host/list/index.js +187 -0
- package/dist/commands/workspace/list/index.d.ts +11 -0
- package/dist/commands/workspace/list/index.js +154 -0
- package/dist/help.d.ts +20 -0
- package/dist/help.js +26 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/oclif.manifest.json +1370 -0
- package/package.json +79 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { Command, 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 inquirer from 'inquirer';
|
|
7
|
+
export default class ProfileWizard extends Command {
|
|
8
|
+
static flags = {
|
|
9
|
+
name: Flags.string({
|
|
10
|
+
char: 'n',
|
|
11
|
+
description: 'Profile name (skip prompt if provided)',
|
|
12
|
+
required: false,
|
|
13
|
+
}),
|
|
14
|
+
origin: Flags.string({
|
|
15
|
+
char: 'o',
|
|
16
|
+
description: 'Xano instance origin URL',
|
|
17
|
+
required: false,
|
|
18
|
+
default: 'https://app.xano.com',
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
static description = 'Create a new profile configuration using an interactive wizard';
|
|
22
|
+
static examples = [
|
|
23
|
+
`$ xano profile:wizard
|
|
24
|
+
Welcome to the Xano Profile Wizard!
|
|
25
|
+
? Enter your access token: ***...***
|
|
26
|
+
? Select an instance:
|
|
27
|
+
> Production (https://app.xano.com)
|
|
28
|
+
Staging (https://staging.xano.com)
|
|
29
|
+
? Profile name: production
|
|
30
|
+
Profile 'production' created successfully at ~/.xano/credentials.yaml
|
|
31
|
+
`,
|
|
32
|
+
];
|
|
33
|
+
async run() {
|
|
34
|
+
const { flags } = await this.parse(ProfileWizard);
|
|
35
|
+
this.log('Welcome to the Xano Profile Wizard!');
|
|
36
|
+
this.log('');
|
|
37
|
+
try {
|
|
38
|
+
// Step 1: Get access token
|
|
39
|
+
const { accessToken } = await inquirer.prompt([
|
|
40
|
+
{
|
|
41
|
+
type: 'password',
|
|
42
|
+
name: 'accessToken',
|
|
43
|
+
message: 'Enter your access token',
|
|
44
|
+
mask: '',
|
|
45
|
+
validate: (input) => {
|
|
46
|
+
if (!input || input.trim() === '') {
|
|
47
|
+
return 'Access token cannot be empty';
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
// Step 2: Fetch instances from API
|
|
54
|
+
this.log('');
|
|
55
|
+
this.log('Fetching available instances...');
|
|
56
|
+
let instances = [];
|
|
57
|
+
try {
|
|
58
|
+
instances = await this.fetchInstances(accessToken, flags.origin);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
this.error(`Failed to fetch instances: ${error instanceof Error ? error.message : String(error)}`);
|
|
62
|
+
}
|
|
63
|
+
if (instances.length === 0) {
|
|
64
|
+
this.error('No instances found. Please check your access token.');
|
|
65
|
+
}
|
|
66
|
+
// Step 3: Let user select an instance
|
|
67
|
+
this.log('');
|
|
68
|
+
const { instanceId } = await inquirer.prompt([
|
|
69
|
+
{
|
|
70
|
+
type: 'list',
|
|
71
|
+
name: 'instanceId',
|
|
72
|
+
message: 'Select an instance',
|
|
73
|
+
choices: instances.map((inst) => ({
|
|
74
|
+
name: `${inst.name} (${inst.display})`,
|
|
75
|
+
value: inst.id,
|
|
76
|
+
})),
|
|
77
|
+
},
|
|
78
|
+
]);
|
|
79
|
+
const selectedInstance = instances.find((inst) => inst.id === instanceId);
|
|
80
|
+
// Step 4: Get profile name
|
|
81
|
+
const defaultProfileName = flags.name || this.getDefaultProfileName();
|
|
82
|
+
const { profileName } = await inquirer.prompt([
|
|
83
|
+
{
|
|
84
|
+
type: 'input',
|
|
85
|
+
name: 'profileName',
|
|
86
|
+
message: 'Profile name',
|
|
87
|
+
default: defaultProfileName,
|
|
88
|
+
validate: (input) => {
|
|
89
|
+
if (!input || input.trim() === '') {
|
|
90
|
+
return 'Profile name cannot be empty';
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
]);
|
|
96
|
+
// Step 5: Workspace selection
|
|
97
|
+
let workspace;
|
|
98
|
+
let branch;
|
|
99
|
+
// Fetch workspaces from the selected instance
|
|
100
|
+
this.log('');
|
|
101
|
+
this.log('Fetching available workspaces...');
|
|
102
|
+
let workspaces = [];
|
|
103
|
+
try {
|
|
104
|
+
workspaces = await this.fetchWorkspaces(accessToken, selectedInstance.origin);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
this.warn(`Failed to fetch workspaces: ${error instanceof Error ? error.message : String(error)}`);
|
|
108
|
+
}
|
|
109
|
+
// If workspaces were fetched, let user select one
|
|
110
|
+
if (workspaces.length > 0) {
|
|
111
|
+
this.log('');
|
|
112
|
+
const { selectedWorkspace } = await inquirer.prompt([
|
|
113
|
+
{
|
|
114
|
+
type: 'list',
|
|
115
|
+
name: 'selectedWorkspace',
|
|
116
|
+
message: 'Select a workspace (or skip to use default)',
|
|
117
|
+
choices: [
|
|
118
|
+
{ name: '(Skip workspace)', value: '' },
|
|
119
|
+
...workspaces.map((ws) => ({
|
|
120
|
+
name: ws.name,
|
|
121
|
+
value: ws.id,
|
|
122
|
+
})),
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
]);
|
|
126
|
+
workspace = selectedWorkspace || undefined;
|
|
127
|
+
// If a workspace was selected, ask about branch preference
|
|
128
|
+
if (workspace) {
|
|
129
|
+
this.log('');
|
|
130
|
+
this.log('');
|
|
131
|
+
this.log('Fetching available branches...');
|
|
132
|
+
let branches = [];
|
|
133
|
+
try {
|
|
134
|
+
branches = await this.fetchBranches(accessToken, selectedInstance.origin, workspace);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
this.warn(`Failed to fetch branches: ${error instanceof Error ? error.message : String(error)}`);
|
|
138
|
+
}
|
|
139
|
+
// If branches were fetched, let user select one
|
|
140
|
+
if (branches.length > 0) {
|
|
141
|
+
this.log('');
|
|
142
|
+
const { selectedBranch } = await inquirer.prompt([
|
|
143
|
+
{
|
|
144
|
+
type: 'list',
|
|
145
|
+
name: 'selectedBranch',
|
|
146
|
+
message: 'Select a branch',
|
|
147
|
+
choices: [
|
|
148
|
+
{ name: '(Skip and use live branch)', value: '' },
|
|
149
|
+
...branches.map((br) => {
|
|
150
|
+
return {
|
|
151
|
+
name: br.label,
|
|
152
|
+
value: br.id,
|
|
153
|
+
};
|
|
154
|
+
}),
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
]);
|
|
158
|
+
branch = selectedBranch || undefined;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Save profile
|
|
163
|
+
await this.saveProfile({
|
|
164
|
+
name: profileName,
|
|
165
|
+
account_origin: flags.origin,
|
|
166
|
+
instance_origin: selectedInstance.origin,
|
|
167
|
+
access_token: accessToken,
|
|
168
|
+
workspace,
|
|
169
|
+
branch,
|
|
170
|
+
}, true);
|
|
171
|
+
this.log('');
|
|
172
|
+
this.log(`✓ Profile '${profileName}' created successfully!`);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (error instanceof Error && error.message.includes('User force closed the prompt')) {
|
|
176
|
+
this.log('Wizard cancelled.');
|
|
177
|
+
process.exit(0);
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async fetchInstances(accessToken, origin) {
|
|
183
|
+
const response = await fetch(`${origin}/api:meta/instance`, {
|
|
184
|
+
method: 'GET',
|
|
185
|
+
headers: {
|
|
186
|
+
accept: 'application/json',
|
|
187
|
+
Authorization: `Bearer ${accessToken}`,
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
if (!response.ok) {
|
|
191
|
+
if (response.status === 401) {
|
|
192
|
+
throw new Error('Unauthorized. Please check your access token.');
|
|
193
|
+
}
|
|
194
|
+
throw new Error(`API request failed with status ${response.status}`);
|
|
195
|
+
}
|
|
196
|
+
const data = (await response.json());
|
|
197
|
+
// Transform API response to Instance format
|
|
198
|
+
// Assuming the API returns an array or object with instances
|
|
199
|
+
if (Array.isArray(data)) {
|
|
200
|
+
return data.map((inst) => ({
|
|
201
|
+
id: inst.id || inst.name,
|
|
202
|
+
name: inst.name,
|
|
203
|
+
display: inst.display,
|
|
204
|
+
origin: new URL(inst.meta_api).origin,
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
// If it's an object, try to extract instances
|
|
208
|
+
if (data && typeof data === 'object') {
|
|
209
|
+
const instances = data.instances || data.data || [];
|
|
210
|
+
if (Array.isArray(instances)) {
|
|
211
|
+
return instances.map((inst) => ({
|
|
212
|
+
id: inst.id || inst.name,
|
|
213
|
+
name: inst.name,
|
|
214
|
+
display: inst.display,
|
|
215
|
+
origin: new URL(inst.meta_api).origin,
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
async fetchWorkspaces(accessToken, origin) {
|
|
222
|
+
const response = await fetch(`${origin}/api:meta/workspace`, {
|
|
223
|
+
method: 'GET',
|
|
224
|
+
headers: {
|
|
225
|
+
accept: 'application/json',
|
|
226
|
+
Authorization: `Bearer ${accessToken}`,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
if (!response.ok) {
|
|
230
|
+
if (response.status === 401) {
|
|
231
|
+
throw new Error('Unauthorized. Please check your access token.');
|
|
232
|
+
}
|
|
233
|
+
throw new Error(`API request failed with status ${response.status}`);
|
|
234
|
+
}
|
|
235
|
+
const data = (await response.json());
|
|
236
|
+
// Transform API response to Workspace format
|
|
237
|
+
// Assuming the API returns an array or object with workspaces
|
|
238
|
+
if (Array.isArray(data)) {
|
|
239
|
+
return data.map((ws) => ({
|
|
240
|
+
id: ws.id || ws.name,
|
|
241
|
+
name: ws.name,
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
// If it's an object, try to extract workspaces
|
|
245
|
+
if (data && typeof data === 'object') {
|
|
246
|
+
const workspaces = data.workspaces || data.data || [];
|
|
247
|
+
if (Array.isArray(workspaces)) {
|
|
248
|
+
return workspaces.map((ws) => ({
|
|
249
|
+
id: ws.id || ws.name,
|
|
250
|
+
name: ws.name,
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
async fetchBranches(accessToken, origin, workspaceId) {
|
|
257
|
+
const response = await fetch(`${origin}/api:meta/workspace/${workspaceId}/branch`, {
|
|
258
|
+
method: 'GET',
|
|
259
|
+
headers: {
|
|
260
|
+
accept: 'application/json',
|
|
261
|
+
Authorization: `Bearer ${accessToken}`,
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
if (!response.ok) {
|
|
265
|
+
if (response.status === 401) {
|
|
266
|
+
throw new Error('Unauthorized. Please check your access token.');
|
|
267
|
+
}
|
|
268
|
+
throw new Error(`API request failed with status ${response.status}`);
|
|
269
|
+
}
|
|
270
|
+
const data = (await response.json());
|
|
271
|
+
// Transform API response to Branch format
|
|
272
|
+
// Assuming the API returns an array or object with branches
|
|
273
|
+
if (Array.isArray(data)) {
|
|
274
|
+
return data.map((br) => ({
|
|
275
|
+
id: br.id || br.label,
|
|
276
|
+
label: br.label,
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
// If it's an object, try to extract branches
|
|
280
|
+
if (data && typeof data === 'object') {
|
|
281
|
+
const branches = data.branches || data.data || [];
|
|
282
|
+
if (Array.isArray(branches)) {
|
|
283
|
+
return branches.map((br) => ({
|
|
284
|
+
id: br.id || br.name,
|
|
285
|
+
label: br.label,
|
|
286
|
+
}));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
getDefaultProfileName() {
|
|
292
|
+
try {
|
|
293
|
+
const configDir = path.join(os.homedir(), '.xano');
|
|
294
|
+
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
295
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
296
|
+
return 'default';
|
|
297
|
+
}
|
|
298
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
299
|
+
const parsed = yaml.load(fileContent);
|
|
300
|
+
if (parsed && typeof parsed === 'object' && 'default' in parsed && parsed.default) {
|
|
301
|
+
return parsed.default;
|
|
302
|
+
}
|
|
303
|
+
return 'default';
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return 'default';
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async saveProfile(profile, setAsDefault = false) {
|
|
310
|
+
const configDir = path.join(os.homedir(), '.xano');
|
|
311
|
+
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
312
|
+
// Ensure the .xano directory exists
|
|
313
|
+
if (!fs.existsSync(configDir)) {
|
|
314
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
315
|
+
}
|
|
316
|
+
// Read existing credentials file or create new structure
|
|
317
|
+
let credentials = { profiles: {} };
|
|
318
|
+
if (fs.existsSync(credentialsPath)) {
|
|
319
|
+
try {
|
|
320
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
321
|
+
const parsed = yaml.load(fileContent);
|
|
322
|
+
if (parsed && typeof parsed === 'object' && 'profiles' in parsed) {
|
|
323
|
+
credentials = parsed;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
// Continue with empty credentials if parse fails
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Add or update the profile
|
|
331
|
+
credentials.profiles[profile.name] = {
|
|
332
|
+
account_origin: profile.account_origin,
|
|
333
|
+
instance_origin: profile.instance_origin,
|
|
334
|
+
access_token: profile.access_token,
|
|
335
|
+
...(profile.workspace && { workspace: profile.workspace }),
|
|
336
|
+
...(profile.branch && { branch: profile.branch }),
|
|
337
|
+
};
|
|
338
|
+
// Set as default if requested
|
|
339
|
+
if (setAsDefault) {
|
|
340
|
+
credentials.default = profile.name;
|
|
341
|
+
}
|
|
342
|
+
// Write the updated credentials back to the file
|
|
343
|
+
const yamlContent = yaml.dump(credentials, {
|
|
344
|
+
indent: 2,
|
|
345
|
+
lineWidth: -1,
|
|
346
|
+
noRefs: true,
|
|
347
|
+
});
|
|
348
|
+
fs.writeFileSync(credentialsPath, yamlContent, 'utf8');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import BaseCommand from '../../../../base-command.js';
|
|
2
|
+
export default class StaticHostBuildCreate extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
static_host: 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
|
+
file: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
name: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
};
|
|
14
|
+
static description: string;
|
|
15
|
+
static examples: string[];
|
|
16
|
+
run(): Promise<void>;
|
|
17
|
+
private loadCredentials;
|
|
18
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
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 StaticHostBuildCreate extends BaseCommand {
|
|
8
|
+
static args = {
|
|
9
|
+
static_host: Args.string({
|
|
10
|
+
description: 'Static Host name',
|
|
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
|
+
file: Flags.string({
|
|
22
|
+
char: 'f',
|
|
23
|
+
description: 'Path to zip file to upload',
|
|
24
|
+
required: true,
|
|
25
|
+
}),
|
|
26
|
+
name: Flags.string({
|
|
27
|
+
char: 'n',
|
|
28
|
+
description: 'Build name',
|
|
29
|
+
required: true,
|
|
30
|
+
}),
|
|
31
|
+
description: Flags.string({
|
|
32
|
+
char: 'd',
|
|
33
|
+
description: 'Build description',
|
|
34
|
+
required: false,
|
|
35
|
+
}),
|
|
36
|
+
output: Flags.string({
|
|
37
|
+
char: 'o',
|
|
38
|
+
description: 'Output format',
|
|
39
|
+
required: false,
|
|
40
|
+
default: 'summary',
|
|
41
|
+
options: ['summary', 'json'],
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
static description = 'Create a new build for a static host';
|
|
45
|
+
static examples = [
|
|
46
|
+
`$ xano static_host:build:create default -f ./build.zip -n "v1.0.0"
|
|
47
|
+
Build created successfully!
|
|
48
|
+
ID: 123
|
|
49
|
+
Name: v1.0.0
|
|
50
|
+
Status: pending
|
|
51
|
+
`,
|
|
52
|
+
`$ xano static_host:build:create default -w 40 -f ./dist.zip -n "production" -d "Production build"
|
|
53
|
+
Build created successfully!
|
|
54
|
+
ID: 124
|
|
55
|
+
Name: production
|
|
56
|
+
Description: Production build
|
|
57
|
+
`,
|
|
58
|
+
`$ xano static_host:build:create myhost -f ./app.zip -n "release-1.2" -o json
|
|
59
|
+
{
|
|
60
|
+
"id": 125,
|
|
61
|
+
"name": "release-1.2",
|
|
62
|
+
"status": "pending"
|
|
63
|
+
}
|
|
64
|
+
`,
|
|
65
|
+
];
|
|
66
|
+
async run() {
|
|
67
|
+
const { args, flags } = await this.parse(StaticHostBuildCreate);
|
|
68
|
+
// Get profile name (default or from flag/env)
|
|
69
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
70
|
+
// Load credentials
|
|
71
|
+
const credentials = this.loadCredentials();
|
|
72
|
+
// Get the profile configuration
|
|
73
|
+
if (!(profileName in credentials.profiles)) {
|
|
74
|
+
this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
75
|
+
`Create a profile using 'xano profile:create'`);
|
|
76
|
+
}
|
|
77
|
+
const profile = credentials.profiles[profileName];
|
|
78
|
+
// Validate required fields
|
|
79
|
+
if (!profile.instance_origin) {
|
|
80
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
81
|
+
}
|
|
82
|
+
if (!profile.access_token) {
|
|
83
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
84
|
+
}
|
|
85
|
+
// Determine workspace_id from flag or profile
|
|
86
|
+
let workspaceId;
|
|
87
|
+
if (flags.workspace) {
|
|
88
|
+
workspaceId = flags.workspace;
|
|
89
|
+
}
|
|
90
|
+
else if (profile.workspace) {
|
|
91
|
+
workspaceId = profile.workspace;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
this.error(`Workspace ID is required. Either:\n` +
|
|
95
|
+
` 1. Provide it as a flag: xano static_host:build:create <static_host> -f <file> -n <name> -w <workspace_id>\n` +
|
|
96
|
+
` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
|
|
97
|
+
}
|
|
98
|
+
// Validate file exists
|
|
99
|
+
const filePath = path.resolve(flags.file);
|
|
100
|
+
if (!fs.existsSync(filePath)) {
|
|
101
|
+
this.error(`File not found: ${filePath}`);
|
|
102
|
+
}
|
|
103
|
+
// Check if file is readable
|
|
104
|
+
try {
|
|
105
|
+
fs.accessSync(filePath, fs.constants.R_OK);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
this.error(`File is not readable: ${filePath}`);
|
|
109
|
+
}
|
|
110
|
+
// Get file stats
|
|
111
|
+
const stats = fs.statSync(filePath);
|
|
112
|
+
if (!stats.isFile()) {
|
|
113
|
+
this.error(`Path is not a file: ${filePath}`);
|
|
114
|
+
}
|
|
115
|
+
// Construct the API URL
|
|
116
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${args.static_host}/build`;
|
|
117
|
+
// Create FormData
|
|
118
|
+
const FormData = (await import('node:buffer')).Blob;
|
|
119
|
+
const formData = new globalThis.FormData();
|
|
120
|
+
// Read file and create blob
|
|
121
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
122
|
+
const blob = new Blob([fileBuffer], { type: 'application/zip' });
|
|
123
|
+
formData.append('file', blob, path.basename(filePath));
|
|
124
|
+
formData.append('name', flags.name);
|
|
125
|
+
if (flags.description) {
|
|
126
|
+
formData.append('description', flags.description);
|
|
127
|
+
}
|
|
128
|
+
// Create build via API
|
|
129
|
+
try {
|
|
130
|
+
const response = await fetch(apiUrl, {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: {
|
|
133
|
+
'accept': 'application/json',
|
|
134
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
135
|
+
},
|
|
136
|
+
body: formData,
|
|
137
|
+
});
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
const errorText = await response.text();
|
|
140
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
141
|
+
}
|
|
142
|
+
const result = await response.json();
|
|
143
|
+
// Validate response
|
|
144
|
+
if (!result || typeof result !== 'object') {
|
|
145
|
+
this.error('Unexpected API response format');
|
|
146
|
+
}
|
|
147
|
+
// Output results
|
|
148
|
+
if (flags.output === 'json') {
|
|
149
|
+
this.log(JSON.stringify(result, null, 2));
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
// summary format
|
|
153
|
+
this.log('Build created successfully!');
|
|
154
|
+
this.log(`ID: ${result.id}`);
|
|
155
|
+
this.log(`Name: ${result.name}`);
|
|
156
|
+
if (result.status) {
|
|
157
|
+
this.log(`Status: ${result.status}`);
|
|
158
|
+
}
|
|
159
|
+
if (flags.description) {
|
|
160
|
+
this.log(`Description: ${flags.description}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (error instanceof Error) {
|
|
166
|
+
this.error(`Failed to create build: ${error.message}`);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
this.error(`Failed to create build: ${String(error)}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
loadCredentials() {
|
|
174
|
+
const configDir = path.join(os.homedir(), '.xano');
|
|
175
|
+
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
176
|
+
// Check if credentials file exists
|
|
177
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
178
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
179
|
+
`Create a profile using 'xano profile:create'`);
|
|
180
|
+
}
|
|
181
|
+
// Read credentials file
|
|
182
|
+
try {
|
|
183
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
184
|
+
const parsed = yaml.load(fileContent);
|
|
185
|
+
if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
|
|
186
|
+
this.error('Credentials file has invalid format.');
|
|
187
|
+
}
|
|
188
|
+
return parsed;
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
this.error(`Failed to parse credentials file: ${error}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import BaseCommand from '../../../../base-command.js';
|
|
2
|
+
export default class StaticHostBuildGet extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
static_host: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
build_id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static flags: {
|
|
8
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
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
|
+
private loadCredentials;
|
|
16
|
+
}
|