@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,279 @@
|
|
|
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 inquirer from 'inquirer';
|
|
7
|
+
import BaseCommand from '../../../base-command.js';
|
|
8
|
+
export default class FunctionGet extends BaseCommand {
|
|
9
|
+
static args = {
|
|
10
|
+
function_id: Args.string({
|
|
11
|
+
description: 'Function ID',
|
|
12
|
+
required: false,
|
|
13
|
+
}),
|
|
14
|
+
};
|
|
15
|
+
static flags = {
|
|
16
|
+
...BaseCommand.baseFlags,
|
|
17
|
+
workspace: Flags.string({
|
|
18
|
+
char: 'w',
|
|
19
|
+
description: 'Workspace ID (optional if set in profile)',
|
|
20
|
+
required: false,
|
|
21
|
+
}),
|
|
22
|
+
output: Flags.string({
|
|
23
|
+
char: 'o',
|
|
24
|
+
description: 'Output format',
|
|
25
|
+
required: false,
|
|
26
|
+
default: 'summary',
|
|
27
|
+
options: ['summary', 'json', 'xs'],
|
|
28
|
+
}),
|
|
29
|
+
include_draft: Flags.boolean({
|
|
30
|
+
description: 'Include draft version',
|
|
31
|
+
required: false,
|
|
32
|
+
default: false,
|
|
33
|
+
}),
|
|
34
|
+
include_xanoscript: Flags.boolean({
|
|
35
|
+
description: 'Include XanoScript in response',
|
|
36
|
+
required: false,
|
|
37
|
+
default: false,
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
static description = 'Get a specific function from a workspace';
|
|
41
|
+
static examples = [
|
|
42
|
+
`$ xano function:get 145 -w 40
|
|
43
|
+
Function: yo (ID: 145)
|
|
44
|
+
Created: 2025-10-10 10:30:00
|
|
45
|
+
Description: Sample function
|
|
46
|
+
`,
|
|
47
|
+
`$ xano function:get 145 --profile production
|
|
48
|
+
Function: yo (ID: 145)
|
|
49
|
+
Created: 2025-10-10 10:30:00
|
|
50
|
+
`,
|
|
51
|
+
`$ xano function:get
|
|
52
|
+
Select a function:
|
|
53
|
+
❯ yo (ID: 145) - Sample function
|
|
54
|
+
another-func (ID: 146)
|
|
55
|
+
`,
|
|
56
|
+
`$ xano function:get 145 -w 40 --output json
|
|
57
|
+
{
|
|
58
|
+
"id": 145,
|
|
59
|
+
"name": "yo",
|
|
60
|
+
"description": "Sample function"
|
|
61
|
+
}
|
|
62
|
+
`,
|
|
63
|
+
`$ xano function:get 145 -p staging -o json --include_draft
|
|
64
|
+
{
|
|
65
|
+
"id": 145,
|
|
66
|
+
"name": "yo"
|
|
67
|
+
}
|
|
68
|
+
`,
|
|
69
|
+
`$ xano function:get 145 -p staging -o xs
|
|
70
|
+
function yo {
|
|
71
|
+
input {
|
|
72
|
+
}
|
|
73
|
+
stack {
|
|
74
|
+
}
|
|
75
|
+
response = null
|
|
76
|
+
}
|
|
77
|
+
`,
|
|
78
|
+
];
|
|
79
|
+
async run() {
|
|
80
|
+
const { args, flags } = await this.parse(FunctionGet);
|
|
81
|
+
// Get profile name (default or from flag/env)
|
|
82
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
83
|
+
// Load credentials
|
|
84
|
+
const credentials = this.loadCredentials();
|
|
85
|
+
// Get the profile configuration
|
|
86
|
+
if (!(profileName in credentials.profiles)) {
|
|
87
|
+
this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
88
|
+
`Create a profile using 'xano profile:create'`);
|
|
89
|
+
}
|
|
90
|
+
const profile = credentials.profiles[profileName];
|
|
91
|
+
// Validate required fields
|
|
92
|
+
if (!profile.instance_origin) {
|
|
93
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
94
|
+
}
|
|
95
|
+
if (!profile.access_token) {
|
|
96
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
97
|
+
}
|
|
98
|
+
// Determine workspace_id from flag or profile
|
|
99
|
+
let workspaceId;
|
|
100
|
+
if (flags.workspace) {
|
|
101
|
+
workspaceId = flags.workspace;
|
|
102
|
+
}
|
|
103
|
+
else if (profile.workspace) {
|
|
104
|
+
workspaceId = profile.workspace;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
this.error(`Workspace ID is required. Either:\n` +
|
|
108
|
+
` 1. Provide it as a flag: xano function:get [function_id] -w <workspace_id>\n` +
|
|
109
|
+
` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
|
|
110
|
+
}
|
|
111
|
+
// If function_id is not provided, prompt user to select from list
|
|
112
|
+
let functionId;
|
|
113
|
+
if (args.function_id) {
|
|
114
|
+
functionId = args.function_id;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
functionId = await this.promptForFunctionId(profile, workspaceId);
|
|
118
|
+
}
|
|
119
|
+
// Build query parameters
|
|
120
|
+
// Automatically set include_xanoscript to true if output format is xs
|
|
121
|
+
const includeXanoscript = flags.output === 'xs' ? true : flags.include_xanoscript;
|
|
122
|
+
const queryParams = new URLSearchParams({
|
|
123
|
+
include_draft: flags.include_draft.toString(),
|
|
124
|
+
include_xanoscript: includeXanoscript.toString(),
|
|
125
|
+
});
|
|
126
|
+
// Construct the API URL
|
|
127
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/function/${functionId}?${queryParams.toString()}`;
|
|
128
|
+
// Fetch function from the API
|
|
129
|
+
try {
|
|
130
|
+
const response = await fetch(apiUrl, {
|
|
131
|
+
method: 'GET',
|
|
132
|
+
headers: {
|
|
133
|
+
'accept': 'application/json',
|
|
134
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
const errorText = await response.text();
|
|
139
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
140
|
+
}
|
|
141
|
+
const func = await response.json();
|
|
142
|
+
// Validate response is an object
|
|
143
|
+
if (!func || typeof func !== 'object') {
|
|
144
|
+
this.error('Unexpected API response format: expected a function object');
|
|
145
|
+
}
|
|
146
|
+
// Output results
|
|
147
|
+
if (flags.output === 'json') {
|
|
148
|
+
this.log(JSON.stringify(func, null, 2));
|
|
149
|
+
}
|
|
150
|
+
else if (flags.output === 'xs') {
|
|
151
|
+
// xs (XanoScript) format - output only the xanoscript element
|
|
152
|
+
if (func.xanoscript) {
|
|
153
|
+
// If status is "ok", output only the value, otherwise output the full xanoscript object
|
|
154
|
+
if (func.xanoscript.status === 'ok' && func.xanoscript.value !== undefined) {
|
|
155
|
+
this.log(func.xanoscript.value);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
this.log(JSON.stringify(func.xanoscript, null, 2));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
this.log('null');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
// summary format
|
|
167
|
+
this.log(`Function: ${func.name} (ID: ${func.id})`);
|
|
168
|
+
if (func.created_at) {
|
|
169
|
+
this.log(`Created: ${func.created_at}`);
|
|
170
|
+
}
|
|
171
|
+
if (func.description) {
|
|
172
|
+
this.log(`Description: ${func.description}`);
|
|
173
|
+
}
|
|
174
|
+
if (func.type) {
|
|
175
|
+
this.log(`Type: ${func.type}`);
|
|
176
|
+
}
|
|
177
|
+
// Don't display xanoscript in summary mode as it can be very large
|
|
178
|
+
if (func.xanoscript) {
|
|
179
|
+
this.log(`XanoScript: (available with -o xs or -o json)`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
if (error instanceof Error) {
|
|
185
|
+
this.error(`Failed to fetch function: ${error.message}`);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
this.error(`Failed to fetch function: ${String(error)}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async promptForFunctionId(profile, workspaceId) {
|
|
193
|
+
try {
|
|
194
|
+
// Fetch list of functions
|
|
195
|
+
const queryParams = new URLSearchParams({
|
|
196
|
+
include_draft: 'false',
|
|
197
|
+
include_xanoscript: 'false',
|
|
198
|
+
page: '1',
|
|
199
|
+
per_page: '50',
|
|
200
|
+
sort: 'created_at',
|
|
201
|
+
order: 'desc',
|
|
202
|
+
});
|
|
203
|
+
const listUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/function?${queryParams.toString()}`;
|
|
204
|
+
const response = await fetch(listUrl, {
|
|
205
|
+
method: 'GET',
|
|
206
|
+
headers: {
|
|
207
|
+
'accept': 'application/json',
|
|
208
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
const errorText = await response.text();
|
|
213
|
+
this.error(`Failed to fetch function list: ${response.status} ${response.statusText}\n${errorText}`);
|
|
214
|
+
}
|
|
215
|
+
const data = await response.json();
|
|
216
|
+
// Handle different response formats
|
|
217
|
+
let functions;
|
|
218
|
+
if (Array.isArray(data)) {
|
|
219
|
+
functions = data;
|
|
220
|
+
}
|
|
221
|
+
else if (data && typeof data === 'object' && 'functions' in data && Array.isArray(data.functions)) {
|
|
222
|
+
functions = data.functions;
|
|
223
|
+
}
|
|
224
|
+
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
225
|
+
functions = data.items;
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
this.error('Unexpected API response format');
|
|
229
|
+
}
|
|
230
|
+
if (functions.length === 0) {
|
|
231
|
+
this.error('No functions found in workspace');
|
|
232
|
+
}
|
|
233
|
+
// Create choices for inquirer
|
|
234
|
+
const choices = functions.map(func => ({
|
|
235
|
+
name: `${func.name} (ID: ${func.id})${func.description ? ` - ${func.description}` : ''}`,
|
|
236
|
+
value: func.id.toString(),
|
|
237
|
+
}));
|
|
238
|
+
// Prompt user to select a function
|
|
239
|
+
const answer = await inquirer.prompt([
|
|
240
|
+
{
|
|
241
|
+
type: 'list',
|
|
242
|
+
name: 'functionId',
|
|
243
|
+
message: 'Select a function:',
|
|
244
|
+
choices,
|
|
245
|
+
},
|
|
246
|
+
]);
|
|
247
|
+
return answer.functionId;
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
if (error instanceof Error) {
|
|
251
|
+
this.error(`Failed to prompt for function: ${error.message}`);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
this.error(`Failed to prompt for function: ${String(error)}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
loadCredentials() {
|
|
259
|
+
const configDir = path.join(os.homedir(), '.xano');
|
|
260
|
+
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
261
|
+
// Check if credentials file exists
|
|
262
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
263
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
264
|
+
`Create a profile using 'xano profile:create'`);
|
|
265
|
+
}
|
|
266
|
+
// Read credentials file
|
|
267
|
+
try {
|
|
268
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
269
|
+
const parsed = yaml.load(fileContent);
|
|
270
|
+
if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
|
|
271
|
+
this.error('Credentials file has invalid format.');
|
|
272
|
+
}
|
|
273
|
+
return parsed;
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
this.error(`Failed to parse credentials file: ${error}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export default class FunctionList extends BaseCommand {
|
|
3
|
+
static args: {};
|
|
4
|
+
static flags: {
|
|
5
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
include_draft: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
include_xanoscript: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
per_page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
sort: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
order: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
};
|
|
15
|
+
static description: string;
|
|
16
|
+
static examples: string[];
|
|
17
|
+
run(): Promise<void>;
|
|
18
|
+
private loadCredentials;
|
|
19
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { 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 FunctionList extends BaseCommand {
|
|
8
|
+
static args = {};
|
|
9
|
+
static flags = {
|
|
10
|
+
...BaseCommand.baseFlags,
|
|
11
|
+
workspace: Flags.string({
|
|
12
|
+
char: 'w',
|
|
13
|
+
description: 'Workspace ID (optional if set in profile)',
|
|
14
|
+
required: false,
|
|
15
|
+
}),
|
|
16
|
+
output: Flags.string({
|
|
17
|
+
char: 'o',
|
|
18
|
+
description: 'Output format',
|
|
19
|
+
required: false,
|
|
20
|
+
default: 'summary',
|
|
21
|
+
options: ['summary', 'json'],
|
|
22
|
+
}),
|
|
23
|
+
include_draft: Flags.boolean({
|
|
24
|
+
description: 'Include draft functions',
|
|
25
|
+
required: false,
|
|
26
|
+
default: false,
|
|
27
|
+
}),
|
|
28
|
+
include_xanoscript: Flags.boolean({
|
|
29
|
+
description: 'Include XanoScript in response',
|
|
30
|
+
required: false,
|
|
31
|
+
default: false,
|
|
32
|
+
}),
|
|
33
|
+
page: Flags.integer({
|
|
34
|
+
description: 'Page number for pagination',
|
|
35
|
+
required: false,
|
|
36
|
+
default: 1,
|
|
37
|
+
}),
|
|
38
|
+
per_page: Flags.integer({
|
|
39
|
+
description: 'Number of results per page',
|
|
40
|
+
required: false,
|
|
41
|
+
default: 50,
|
|
42
|
+
}),
|
|
43
|
+
sort: Flags.string({
|
|
44
|
+
description: 'Sort field',
|
|
45
|
+
required: false,
|
|
46
|
+
default: 'created_at',
|
|
47
|
+
}),
|
|
48
|
+
order: Flags.string({
|
|
49
|
+
description: 'Sort order',
|
|
50
|
+
required: false,
|
|
51
|
+
default: 'desc',
|
|
52
|
+
options: ['asc', 'desc'],
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
static description = 'List all functions in a workspace from the Xano Metadata API';
|
|
56
|
+
static examples = [
|
|
57
|
+
`$ xano function:list -w 40
|
|
58
|
+
Available functions:
|
|
59
|
+
- function-1 (ID: 1)
|
|
60
|
+
- function-2 (ID: 2)
|
|
61
|
+
- function-3 (ID: 3)
|
|
62
|
+
`,
|
|
63
|
+
`$ xano function:list --profile production
|
|
64
|
+
Available functions:
|
|
65
|
+
- my-function (ID: 1)
|
|
66
|
+
- another-function (ID: 2)
|
|
67
|
+
`,
|
|
68
|
+
`$ xano function:list -w 40 --output json
|
|
69
|
+
[
|
|
70
|
+
{
|
|
71
|
+
"id": 1,
|
|
72
|
+
"name": "function-1"
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
`,
|
|
76
|
+
`$ xano function:list -p staging -o json --include_draft
|
|
77
|
+
[
|
|
78
|
+
{
|
|
79
|
+
"id": 1,
|
|
80
|
+
"name": "function-1"
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
`,
|
|
84
|
+
];
|
|
85
|
+
async run() {
|
|
86
|
+
const { flags } = await this.parse(FunctionList);
|
|
87
|
+
// Get profile name (default or from flag/env)
|
|
88
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
89
|
+
// Load credentials
|
|
90
|
+
const credentials = this.loadCredentials();
|
|
91
|
+
// Get the profile configuration
|
|
92
|
+
if (!(profileName in credentials.profiles)) {
|
|
93
|
+
this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
94
|
+
`Create a profile using 'xano profile:create'`);
|
|
95
|
+
}
|
|
96
|
+
const profile = credentials.profiles[profileName];
|
|
97
|
+
// Validate required fields
|
|
98
|
+
if (!profile.instance_origin) {
|
|
99
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
100
|
+
}
|
|
101
|
+
if (!profile.access_token) {
|
|
102
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
103
|
+
}
|
|
104
|
+
// Determine workspace_id from flag or profile
|
|
105
|
+
let workspaceId;
|
|
106
|
+
if (flags.workspace) {
|
|
107
|
+
workspaceId = flags.workspace;
|
|
108
|
+
}
|
|
109
|
+
else if (profile.workspace) {
|
|
110
|
+
workspaceId = profile.workspace;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
this.error(`Workspace ID is required. Either:\n` +
|
|
114
|
+
` 1. Provide it as a flag: xano function:list -w <workspace_id>\n` +
|
|
115
|
+
` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
|
|
116
|
+
}
|
|
117
|
+
// Build query parameters
|
|
118
|
+
const queryParams = new URLSearchParams({
|
|
119
|
+
include_draft: flags.include_draft.toString(),
|
|
120
|
+
include_xanoscript: flags.include_xanoscript.toString(),
|
|
121
|
+
page: flags.page.toString(),
|
|
122
|
+
per_page: flags.per_page.toString(),
|
|
123
|
+
sort: flags.sort,
|
|
124
|
+
order: flags.order,
|
|
125
|
+
});
|
|
126
|
+
// Construct the API URL
|
|
127
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/function?${queryParams.toString()}`;
|
|
128
|
+
// Fetch functions from the API
|
|
129
|
+
try {
|
|
130
|
+
const response = await fetch(apiUrl, {
|
|
131
|
+
method: 'GET',
|
|
132
|
+
headers: {
|
|
133
|
+
'accept': 'application/json',
|
|
134
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
const errorText = await response.text();
|
|
139
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
140
|
+
}
|
|
141
|
+
const data = await response.json();
|
|
142
|
+
// Handle different response formats
|
|
143
|
+
let functions;
|
|
144
|
+
if (Array.isArray(data)) {
|
|
145
|
+
functions = data;
|
|
146
|
+
}
|
|
147
|
+
else if (data && typeof data === 'object' && 'functions' in data && Array.isArray(data.functions)) {
|
|
148
|
+
functions = data.functions;
|
|
149
|
+
}
|
|
150
|
+
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
151
|
+
functions = data.items;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
this.error('Unexpected API response format');
|
|
155
|
+
}
|
|
156
|
+
// Output results
|
|
157
|
+
if (flags.output === 'json') {
|
|
158
|
+
this.log(JSON.stringify(functions, null, 2));
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// summary format
|
|
162
|
+
if (functions.length === 0) {
|
|
163
|
+
this.log('No functions found');
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
this.log('Available functions:');
|
|
167
|
+
for (const func of functions) {
|
|
168
|
+
if (func.id !== undefined) {
|
|
169
|
+
this.log(` - ${func.name} (ID: ${func.id})`);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
this.log(` - ${func.name}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
if (error instanceof Error) {
|
|
180
|
+
this.error(`Failed to fetch functions: ${error.message}`);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
this.error(`Failed to fetch functions: ${String(error)}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
loadCredentials() {
|
|
188
|
+
const configDir = path.join(os.homedir(), '.xano');
|
|
189
|
+
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
190
|
+
// Check if credentials file exists
|
|
191
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
192
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
193
|
+
`Create a profile using 'xano profile:create'`);
|
|
194
|
+
}
|
|
195
|
+
// Read credentials file
|
|
196
|
+
try {
|
|
197
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
198
|
+
const parsed = yaml.load(fileContent);
|
|
199
|
+
if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
|
|
200
|
+
this.error('Credentials file has invalid format.');
|
|
201
|
+
}
|
|
202
|
+
return parsed;
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
this.error(`Failed to parse credentials file: ${error}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class ProfileCreate extends Command {
|
|
3
|
+
static args: {
|
|
4
|
+
name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static flags: {
|
|
7
|
+
account_origin: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
instance_origin: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
access_token: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
default: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
};
|
|
14
|
+
static description: string;
|
|
15
|
+
static examples: string[];
|
|
16
|
+
run(): Promise<void>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Args, 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
|
+
export default class ProfileCreate extends Command {
|
|
7
|
+
static args = {
|
|
8
|
+
name: Args.string({
|
|
9
|
+
description: 'Profile name',
|
|
10
|
+
required: true,
|
|
11
|
+
}),
|
|
12
|
+
};
|
|
13
|
+
static flags = {
|
|
14
|
+
account_origin: Flags.string({
|
|
15
|
+
char: 'a',
|
|
16
|
+
description: 'Account origin URL. Optional for self hosted installs.',
|
|
17
|
+
required: false,
|
|
18
|
+
}),
|
|
19
|
+
instance_origin: Flags.string({
|
|
20
|
+
char: 'i',
|
|
21
|
+
description: 'Instance origin URL',
|
|
22
|
+
required: true,
|
|
23
|
+
}),
|
|
24
|
+
access_token: Flags.string({
|
|
25
|
+
char: 't',
|
|
26
|
+
description: 'Access token for the Xano Metadata API',
|
|
27
|
+
required: true,
|
|
28
|
+
}),
|
|
29
|
+
workspace: Flags.string({
|
|
30
|
+
char: 'w',
|
|
31
|
+
description: 'Workspace name',
|
|
32
|
+
required: false,
|
|
33
|
+
}),
|
|
34
|
+
branch: Flags.string({
|
|
35
|
+
char: 'b',
|
|
36
|
+
description: 'Branch name',
|
|
37
|
+
required: false,
|
|
38
|
+
}),
|
|
39
|
+
default: Flags.boolean({
|
|
40
|
+
description: 'Set this profile as the default',
|
|
41
|
+
required: false,
|
|
42
|
+
default: false,
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
static description = 'Create a new profile configuration';
|
|
46
|
+
static examples = [
|
|
47
|
+
`$ xano profile:create production --account_origin https://account.xano.com --instance_origin https://instance.xano.com --access_token token123
|
|
48
|
+
Profile 'production' created successfully at ~/.xano/credentials.yaml
|
|
49
|
+
`,
|
|
50
|
+
`$ xano profile:create staging -a https://staging-account.xano.com -i https://staging-instance.xano.com -t token456
|
|
51
|
+
Profile 'staging' created successfully at ~/.xano/credentials.yaml
|
|
52
|
+
`,
|
|
53
|
+
`$ xano profile:create dev -i https://dev-instance.xano.com -t token789 -w my-workspace -b feature-branch
|
|
54
|
+
Profile 'dev' created successfully at ~/.xano/credentials.yaml
|
|
55
|
+
`,
|
|
56
|
+
`$ xano profile:create production --account_origin https://account.xano.com --instance_origin https://instance.xano.com --access_token token123 --default
|
|
57
|
+
Profile 'production' created successfully at ~/.xano/credentials.yaml
|
|
58
|
+
Default profile set to 'production'
|
|
59
|
+
`,
|
|
60
|
+
];
|
|
61
|
+
async run() {
|
|
62
|
+
const { args, flags } = await this.parse(ProfileCreate);
|
|
63
|
+
const configDir = path.join(os.homedir(), '.xano');
|
|
64
|
+
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
65
|
+
// Ensure the .xano directory exists
|
|
66
|
+
if (!fs.existsSync(configDir)) {
|
|
67
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
68
|
+
this.log(`Created directory: ${configDir}`);
|
|
69
|
+
}
|
|
70
|
+
// Read existing credentials file or create new structure
|
|
71
|
+
let credentials = { profiles: {} };
|
|
72
|
+
if (fs.existsSync(credentialsPath)) {
|
|
73
|
+
try {
|
|
74
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
75
|
+
const parsed = yaml.load(fileContent);
|
|
76
|
+
if (parsed && typeof parsed === 'object' && 'profiles' in parsed) {
|
|
77
|
+
credentials = parsed;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
this.warn('Existing credentials file has invalid format. Creating new structure.');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
this.warn(`Failed to parse existing credentials file: ${error}`);
|
|
85
|
+
this.warn('Creating new credentials file.');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Add or update the profile
|
|
89
|
+
const profileExists = args.name in credentials.profiles;
|
|
90
|
+
credentials.profiles[args.name] = {
|
|
91
|
+
account_origin: flags.account_origin ?? '',
|
|
92
|
+
instance_origin: flags.instance_origin,
|
|
93
|
+
access_token: flags.access_token,
|
|
94
|
+
...(flags.workspace && { workspace: flags.workspace }),
|
|
95
|
+
...(flags.branch && { branch: flags.branch }),
|
|
96
|
+
};
|
|
97
|
+
// Set default if flag is provided
|
|
98
|
+
if (flags.default) {
|
|
99
|
+
credentials.default = args.name;
|
|
100
|
+
}
|
|
101
|
+
// Write the updated credentials back to the file
|
|
102
|
+
try {
|
|
103
|
+
const yamlContent = yaml.dump(credentials, {
|
|
104
|
+
indent: 2,
|
|
105
|
+
lineWidth: -1,
|
|
106
|
+
noRefs: true,
|
|
107
|
+
});
|
|
108
|
+
fs.writeFileSync(credentialsPath, yamlContent, 'utf8');
|
|
109
|
+
if (profileExists) {
|
|
110
|
+
this.log(`Profile '${args.name}' updated successfully at ${credentialsPath}`);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
this.log(`Profile '${args.name}' created successfully at ${credentialsPath}`);
|
|
114
|
+
}
|
|
115
|
+
if (flags.default) {
|
|
116
|
+
this.log(`Default profile set to '${args.name}'`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
this.error(`Failed to write credentials file: ${error}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class ProfileDelete extends Command {
|
|
3
|
+
static args: {
|
|
4
|
+
name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static flags: {
|
|
7
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
};
|
|
9
|
+
static description: string;
|
|
10
|
+
static examples: string[];
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
private confirm;
|
|
13
|
+
private promptInput;
|
|
14
|
+
}
|