@xano/cli 1.0.4 → 1.0.6-beta.0
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 +139 -4
- package/dist/base-command.d.ts +27 -0
- package/dist/base-command.js +124 -1
- package/dist/commands/auth/index.d.ts +45 -1
- package/dist/commands/auth/index.js +197 -29
- package/dist/commands/ephemeral/create/index.d.ts +18 -0
- package/dist/commands/ephemeral/create/index.js +103 -0
- package/dist/commands/ephemeral/delete/index.d.ts +18 -0
- package/dist/commands/ephemeral/delete/index.js +99 -0
- package/dist/commands/ephemeral/edit/index.d.ts +18 -0
- package/dist/commands/ephemeral/edit/index.js +109 -0
- package/dist/commands/ephemeral/get/index.d.ts +21 -0
- package/dist/commands/ephemeral/get/index.js +125 -0
- package/dist/commands/ephemeral/impersonate/index.d.ts +19 -0
- package/dist/commands/ephemeral/impersonate/index.js +112 -0
- package/dist/commands/ephemeral/list/index.d.ts +19 -0
- package/dist/commands/ephemeral/list/index.js +122 -0
- package/dist/commands/ephemeral/pull/index.d.ts +23 -0
- package/dist/commands/ephemeral/pull/index.js +248 -0
- package/dist/commands/{knowledge → ephemeral}/push/index.d.ts +8 -5
- package/dist/commands/ephemeral/push/index.js +143 -0
- package/dist/commands/function/run/index.d.ts +40 -0
- package/dist/commands/function/run/index.js +244 -0
- package/dist/commands/knowledge/{pull → get}/index.d.ts +6 -2
- package/dist/commands/knowledge/get/index.js +126 -0
- package/dist/commands/knowledge/list/index.d.ts +18 -0
- package/dist/commands/knowledge/list/index.js +135 -0
- package/dist/commands/sandbox/pull/index.js +12 -2
- package/dist/commands/sandbox/push/index.d.ts +1 -1
- package/dist/commands/sandbox/push/index.js +19 -14
- package/dist/commands/sandbox/reset/index.js +12 -0
- package/dist/commands/static_host/build/create/index.d.ts +9 -1
- package/dist/commands/static_host/build/create/index.js +54 -4
- package/dist/commands/static_host/build/delete/index.d.ts +19 -0
- package/dist/commands/static_host/build/delete/index.js +114 -0
- package/dist/commands/static_host/build/get/index.d.ts +1 -1
- package/dist/commands/static_host/build/get/index.js +16 -10
- package/dist/commands/static_host/build/pull/index.d.ts +52 -0
- package/dist/commands/static_host/build/pull/index.js +300 -0
- package/dist/commands/static_host/build/push/index.d.ts +23 -0
- package/dist/commands/static_host/build/push/index.js +228 -0
- package/dist/commands/static_host/create/index.d.ts +17 -0
- package/dist/commands/static_host/create/index.js +86 -0
- package/dist/commands/static_host/deploy/index.d.ts +18 -0
- package/dist/commands/static_host/deploy/index.js +105 -0
- package/dist/commands/static_host/edit/index.d.ts +23 -0
- package/dist/commands/static_host/edit/index.js +151 -0
- package/dist/commands/static_host/get/index.d.ts +18 -0
- package/dist/commands/static_host/get/index.js +94 -0
- package/dist/commands/static_host/migrate/index.d.ts +44 -0
- package/dist/commands/static_host/migrate/index.js +205 -0
- package/dist/commands/tenant/snapshot/create/index.d.ts +17 -0
- package/dist/commands/tenant/snapshot/create/index.js +78 -0
- package/dist/commands/tenant/snapshot/delete/index.d.ts +19 -0
- package/dist/commands/tenant/snapshot/delete/index.js +102 -0
- package/dist/commands/tenant/snapshot/list/index.d.ts +16 -0
- package/dist/commands/tenant/snapshot/list/index.js +96 -0
- package/dist/commands/tenant/snapshot/swap/index.d.ts +19 -0
- package/dist/commands/tenant/snapshot/swap/index.js +103 -0
- package/dist/commands/tenant/unit_test/run/index.d.ts +1 -0
- package/dist/commands/tenant/unit_test/run/index.js +7 -0
- package/dist/commands/tenant/unit_test/run_all/index.js +7 -3
- package/dist/commands/unit_test/run/index.d.ts +1 -0
- package/dist/commands/unit_test/run/index.js +8 -0
- package/dist/commands/unit_test/run_all/index.js +7 -3
- package/dist/commands/workspace/pull/index.js +13 -2
- package/dist/commands/workspace/push/index.d.ts +1 -1
- package/dist/commands/workspace/push/index.js +15 -5
- package/dist/utils/function-input.d.ts +77 -0
- package/dist/utils/function-input.js +148 -0
- package/dist/utils/knowledge-sync.d.ts +84 -79
- package/dist/utils/knowledge-sync.js +376 -323
- package/dist/utils/multidoc-push.d.ts +30 -18
- package/dist/utils/multidoc-push.js +284 -148
- package/dist/utils/reference-checker.js +2 -2
- package/dist/utils/static-host-files.d.ts +21 -0
- package/dist/utils/static-host-files.js +41 -0
- package/oclif.manifest.json +5046 -2691
- package/package.json +10 -1
- package/dist/commands/knowledge/pull/index.js +0 -86
- package/dist/commands/knowledge/push/index.js +0 -126
|
@@ -6,9 +6,37 @@ import * as http from 'node:http';
|
|
|
6
6
|
import { dirname } from 'node:path';
|
|
7
7
|
import open from 'open';
|
|
8
8
|
import { buildUserAgent, resolveCredentialsPath } from '../../base-command.js';
|
|
9
|
+
function originHostname(value) {
|
|
10
|
+
try {
|
|
11
|
+
return new URL(value.includes('://') ? value : `https://${value}`).hostname.toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Match a user-supplied --instance value against the instance list:
|
|
19
|
+
* numeric values match by ID, URL/hostname values match by the instance
|
|
20
|
+
* origin's hostname, anything else matches by name. Exported for tests.
|
|
21
|
+
*/
|
|
22
|
+
export function matchInstance(instances, query) {
|
|
23
|
+
const q = query.trim();
|
|
24
|
+
if (/^\d+$/.test(q)) {
|
|
25
|
+
return instances.find((inst) => inst.id === q);
|
|
26
|
+
}
|
|
27
|
+
if (q.includes('://') || q.includes('.')) {
|
|
28
|
+
const queryHost = originHostname(q);
|
|
29
|
+
const match = queryHost ? instances.find((inst) => originHostname(inst.origin) === queryHost) : undefined;
|
|
30
|
+
if (match) {
|
|
31
|
+
return match;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return instances.find((inst) => inst.name === q);
|
|
35
|
+
}
|
|
9
36
|
const AUTH_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
10
37
|
export default class Auth extends Command {
|
|
11
38
|
static description = 'Authenticate with Xano via browser login';
|
|
39
|
+
static enableJsonFlag = true;
|
|
12
40
|
static examples = [
|
|
13
41
|
`$ xano auth
|
|
14
42
|
Opening browser for Xano login...
|
|
@@ -23,10 +51,24 @@ Opening browser for Xano login at https://custom.xano.com...`,
|
|
|
23
51
|
To authenticate, open the following URL in any browser:
|
|
24
52
|
https://app.xano.com/login?dest=cli&display=code
|
|
25
53
|
? Paste the code shown in your browser: ****`,
|
|
26
|
-
`$
|
|
27
|
-
(
|
|
54
|
+
`$ xano auth --code "$CODE" --instance https://my-instance.xano.io --workspace 5
|
|
55
|
+
(fully non-interactive: no browser, no prompts; missing --branch/--profile fall back to defaults)`,
|
|
56
|
+
`$ echo "$CODE" | xano auth --no-browser --instance my-instance --workspace 5 --branch dev --profile staging
|
|
57
|
+
(fully scripted: the code is read from piped stdin, no prompt at all)`,
|
|
58
|
+
`$ xano auth --code "$CODE" --instance 42 --workspace 5 --json
|
|
59
|
+
(machine-readable: prints the created profile as JSON)`,
|
|
28
60
|
];
|
|
29
61
|
static flags = {
|
|
62
|
+
branch: Flags.string({
|
|
63
|
+
char: 'b',
|
|
64
|
+
description: 'Pre-select a branch by label (skips the branch picker); pass "" to skip and use the live branch',
|
|
65
|
+
required: false,
|
|
66
|
+
}),
|
|
67
|
+
code: Flags.string({
|
|
68
|
+
description: 'Login code copied from the browser (implies --no-browser and runs fully non-interactively). Get the code at <origin>/login?dest=cli&display=code',
|
|
69
|
+
env: 'XANO_AUTH_CODE',
|
|
70
|
+
required: false,
|
|
71
|
+
}),
|
|
30
72
|
config: Flags.string({
|
|
31
73
|
char: 'c',
|
|
32
74
|
description: 'Path to credentials file (default: ~/.xano/credentials.yaml)',
|
|
@@ -38,6 +80,11 @@ To authenticate, open the following URL in any browser:
|
|
|
38
80
|
default: false,
|
|
39
81
|
description: 'Skip TLS certificate verification (for self-signed certificates)',
|
|
40
82
|
}),
|
|
83
|
+
instance: Flags.string({
|
|
84
|
+
char: 'i',
|
|
85
|
+
description: 'Pre-select an instance by name, numeric ID, or instance URL/hostname (skips the instance picker)',
|
|
86
|
+
required: false,
|
|
87
|
+
}),
|
|
41
88
|
'no-browser': Flags.boolean({
|
|
42
89
|
default: false,
|
|
43
90
|
description: 'Headless login: print a URL and paste back the code shown in the browser, instead of starting a local callback server (use on remote/SSH/Docker hosts where 127.0.0.1 is not reachable from the browser)',
|
|
@@ -47,6 +94,16 @@ To authenticate, open the following URL in any browser:
|
|
|
47
94
|
default: 'https://app.xano.com',
|
|
48
95
|
description: 'Xano account origin URL',
|
|
49
96
|
}),
|
|
97
|
+
profile: Flags.string({
|
|
98
|
+
char: 'p',
|
|
99
|
+
description: 'Profile name to save (skips the profile name prompt); pass "" to use the default name',
|
|
100
|
+
required: false,
|
|
101
|
+
}),
|
|
102
|
+
workspace: Flags.string({
|
|
103
|
+
char: 'w',
|
|
104
|
+
description: 'Pre-select a workspace by ID or name (skips the workspace picker); pass "" to skip workspace',
|
|
105
|
+
required: false,
|
|
106
|
+
}),
|
|
50
107
|
};
|
|
51
108
|
async run() {
|
|
52
109
|
const { flags } = await this.parse(Auth);
|
|
@@ -54,12 +111,13 @@ To authenticate, open the following URL in any browser:
|
|
|
54
111
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
55
112
|
this.warn('TLS certificate verification is disabled (insecure mode)');
|
|
56
113
|
}
|
|
114
|
+
// A supplied code (flag or env), piped stdin, or --json output means no
|
|
115
|
+
// prompt can ever open: every unanswered picker falls back to its default.
|
|
116
|
+
const nonInteractive = flags.code !== undefined || (flags['no-browser'] && !process.stdin.isTTY) || this.jsonEnabled();
|
|
57
117
|
try {
|
|
58
|
-
// Step 1: Get token via browser auth
|
|
118
|
+
// Step 1: Get token via supplied code or browser auth
|
|
59
119
|
this.log('Starting authentication flow...');
|
|
60
|
-
const token = flags['no-browser']
|
|
61
|
-
? await this.promptForToken(flags.origin)
|
|
62
|
-
: await this.startAuthServer(flags.origin);
|
|
120
|
+
const token = await this.acquireToken(flags.code, flags['no-browser'], flags.origin);
|
|
63
121
|
// Step 2: Validate token and get user info
|
|
64
122
|
this.log('');
|
|
65
123
|
this.log('Validating authentication...');
|
|
@@ -71,6 +129,9 @@ To authenticate, open the following URL in any browser:
|
|
|
71
129
|
const isSelfHosted = !/^https:\/\/app\.(.*\.)?xano\.com$/.test(flags.origin);
|
|
72
130
|
let instance;
|
|
73
131
|
if (isSelfHosted) {
|
|
132
|
+
if (flags.instance) {
|
|
133
|
+
this.warn('Ignoring --instance: the origin itself is the instance for self-hosted Xano.');
|
|
134
|
+
}
|
|
74
135
|
instance = {
|
|
75
136
|
display: flags.origin,
|
|
76
137
|
id: 'self-hosted',
|
|
@@ -85,31 +146,15 @@ To authenticate, open the following URL in any browser:
|
|
|
85
146
|
if (instances.length === 0) {
|
|
86
147
|
this.error('No instances found. Please check your account.');
|
|
87
148
|
}
|
|
88
|
-
instance = await this.
|
|
89
|
-
}
|
|
90
|
-
// Step 4: Workspace selection
|
|
91
|
-
let workspace;
|
|
92
|
-
let branch;
|
|
93
|
-
this.log('');
|
|
94
|
-
this.log('Fetching available workspaces...');
|
|
95
|
-
const workspaces = await this.fetchWorkspaces(token, instance.origin);
|
|
96
|
-
if (workspaces.length > 0) {
|
|
97
|
-
workspace = await this.selectWorkspace(workspaces);
|
|
98
|
-
if (workspace) {
|
|
99
|
-
// Step 5: Branch selection
|
|
100
|
-
this.log('');
|
|
101
|
-
this.log('Fetching available branches...');
|
|
102
|
-
const branches = await this.fetchBranches(token, instance.origin, workspace.id);
|
|
103
|
-
if (branches.length > 1) {
|
|
104
|
-
branch = await this.selectBranch(branches);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
149
|
+
instance = await this.resolveInstance(instances, flags.instance, nonInteractive);
|
|
107
150
|
}
|
|
151
|
+
// Steps 4 + 5: Workspace and branch selection
|
|
152
|
+
const { branch, workspace } = await this.resolveWorkspaceAndBranch(token, instance.origin, flags, nonInteractive);
|
|
108
153
|
// Step 6: Profile name
|
|
109
154
|
this.log('');
|
|
110
|
-
const profileName = await this.
|
|
155
|
+
const profileName = await this.resolveProfileName(flags.profile, nonInteractive);
|
|
111
156
|
// Step 7: Save profile
|
|
112
|
-
await this.saveProfile({
|
|
157
|
+
const credentialsPath = await this.saveProfile({
|
|
113
158
|
access_token: token,
|
|
114
159
|
account_origin: flags.origin,
|
|
115
160
|
branch,
|
|
@@ -120,6 +165,17 @@ To authenticate, open the following URL in any browser:
|
|
|
120
165
|
}, flags.config);
|
|
121
166
|
this.log('');
|
|
122
167
|
this.log(`Profile '${profileName}' created successfully!`);
|
|
168
|
+
const result = {
|
|
169
|
+
branch: branch ?? null,
|
|
170
|
+
credentialsPath,
|
|
171
|
+
instance: { id: instance.id, name: instance.name, origin: instance.origin },
|
|
172
|
+
profile: profileName,
|
|
173
|
+
user: { email: user.email, id: user.id, name: user.name },
|
|
174
|
+
workspace: workspace ? { id: workspace.id, name: workspace.name } : null,
|
|
175
|
+
};
|
|
176
|
+
if (this.jsonEnabled()) {
|
|
177
|
+
this.logJson(result);
|
|
178
|
+
}
|
|
123
179
|
// Ensure clean exit (the open() call can keep the event loop alive)
|
|
124
180
|
process.exit(0);
|
|
125
181
|
}
|
|
@@ -129,11 +185,33 @@ To authenticate, open the following URL in any browser:
|
|
|
129
185
|
// @inquirer/core, so the thrown class won't match an imported one.
|
|
130
186
|
if (error?.name === 'ExitPromptError') {
|
|
131
187
|
this.log('Authentication cancelled.');
|
|
132
|
-
|
|
188
|
+
this.exit(0);
|
|
133
189
|
}
|
|
134
190
|
throw error;
|
|
135
191
|
}
|
|
136
192
|
}
|
|
193
|
+
// oclif's default toErrorJson serializes Error objects to {} (message is a
|
|
194
|
+
// non-enumerable property), leaving --json consumers with no error detail.
|
|
195
|
+
toErrorJson(err) {
|
|
196
|
+
const error = err;
|
|
197
|
+
return {
|
|
198
|
+
error: {
|
|
199
|
+
...(error.code ? { code: error.code } : {}),
|
|
200
|
+
message: error.message ?? String(err),
|
|
201
|
+
...(error.suggestions?.length ? { suggestions: error.suggestions } : {}),
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
async acquireToken(code, noBrowser, origin) {
|
|
206
|
+
if (code !== undefined) {
|
|
207
|
+
const token = code.trim();
|
|
208
|
+
if (token === '') {
|
|
209
|
+
this.error(`No code provided. Copy it from ${origin}/login?dest=cli&display=code and pass it via --code or XANO_AUTH_CODE.`);
|
|
210
|
+
}
|
|
211
|
+
return token;
|
|
212
|
+
}
|
|
213
|
+
return noBrowser ? this.promptForToken(origin) : this.startAuthServer(origin);
|
|
214
|
+
}
|
|
137
215
|
getHeaders(accessToken) {
|
|
138
216
|
return {
|
|
139
217
|
'User-Agent': buildUserAgent(this.config.version),
|
|
@@ -181,7 +259,7 @@ To authenticate, open the following URL in any browser:
|
|
|
181
259
|
if (Array.isArray(data)) {
|
|
182
260
|
return data.map((inst) => ({
|
|
183
261
|
display: inst.display,
|
|
184
|
-
id: inst.id
|
|
262
|
+
id: String(inst.id ?? inst.name),
|
|
185
263
|
name: inst.name,
|
|
186
264
|
origin: new URL(inst.meta_api).origin,
|
|
187
265
|
}));
|
|
@@ -278,6 +356,95 @@ To authenticate, open the following URL in any browser:
|
|
|
278
356
|
process.stdin.on('error', () => resolve(data.trim()));
|
|
279
357
|
});
|
|
280
358
|
}
|
|
359
|
+
async resolveBranch(branches, flagValue, nonInteractive) {
|
|
360
|
+
if (flagValue !== undefined) {
|
|
361
|
+
// An empty value means "skip and use live branch" (same as the picker's skip option)
|
|
362
|
+
if (flagValue.trim() === '') {
|
|
363
|
+
this.log('Using live branch');
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
const match = branches.find((br) => br.label === flagValue || br.id === flagValue);
|
|
367
|
+
if (!match) {
|
|
368
|
+
this.error(`Branch '${flagValue}' not found. Available branches: ${branches.map((br) => br.label).join(', ')}`);
|
|
369
|
+
}
|
|
370
|
+
this.log(`Using branch: ${match.label}`);
|
|
371
|
+
return match.id;
|
|
372
|
+
}
|
|
373
|
+
if (nonInteractive) {
|
|
374
|
+
this.log('Using live branch (non-interactive; pass --branch to select one)');
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
return branches.length > 1 ? this.selectBranch(branches) : undefined;
|
|
378
|
+
}
|
|
379
|
+
async resolveInstance(instances, flagValue, nonInteractive) {
|
|
380
|
+
if (flagValue) {
|
|
381
|
+
const match = matchInstance(instances, flagValue);
|
|
382
|
+
if (!match) {
|
|
383
|
+
this.error(`Instance '${flagValue}' not found (match by name, numeric ID, or instance URL). Available instances: ${instances.map((inst) => `${inst.name} (${inst.id})`).join(', ')}`);
|
|
384
|
+
}
|
|
385
|
+
this.log(`Using instance: ${match.name} (${match.display})`);
|
|
386
|
+
return match;
|
|
387
|
+
}
|
|
388
|
+
if (nonInteractive) {
|
|
389
|
+
if (instances.length === 1) {
|
|
390
|
+
this.log(`Using instance: ${instances[0].name} (${instances[0].display})`);
|
|
391
|
+
return instances[0];
|
|
392
|
+
}
|
|
393
|
+
this.error(`Multiple instances available; pass --instance to select one. Available instances: ${instances.map((inst) => `${inst.name} (${inst.id})`).join(', ')}`);
|
|
394
|
+
}
|
|
395
|
+
return this.selectInstance(instances);
|
|
396
|
+
}
|
|
397
|
+
async resolveProfileName(flagValue, nonInteractive) {
|
|
398
|
+
// An empty --profile value means "use the default name" (same as accepting the prompt's default);
|
|
399
|
+
// non-interactive runs with no --profile fall back to the default name too.
|
|
400
|
+
if (flagValue !== undefined) {
|
|
401
|
+
return flagValue.trim() || 'default';
|
|
402
|
+
}
|
|
403
|
+
return nonInteractive ? 'default' : this.promptProfileName();
|
|
404
|
+
}
|
|
405
|
+
async resolveWorkspace(workspaces, flagValue, nonInteractive) {
|
|
406
|
+
if (flagValue !== undefined) {
|
|
407
|
+
// An empty value means "skip workspace" (same as the picker's skip option)
|
|
408
|
+
if (flagValue.trim() === '') {
|
|
409
|
+
this.log('Skipping workspace selection');
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
const match = workspaces.find((ws) => String(ws.id) === flagValue || ws.name === flagValue);
|
|
413
|
+
if (!match) {
|
|
414
|
+
this.error(`Workspace '${flagValue}' not found. Available workspaces: ${workspaces.map((ws) => `${ws.name} (${ws.id})`).join(', ')}`);
|
|
415
|
+
}
|
|
416
|
+
this.log(`Using workspace: ${match.name} (${match.id})`);
|
|
417
|
+
return match;
|
|
418
|
+
}
|
|
419
|
+
if (nonInteractive) {
|
|
420
|
+
this.log('Skipping workspace selection (non-interactive; pass --workspace to select one)');
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
423
|
+
return this.selectWorkspace(workspaces);
|
|
424
|
+
}
|
|
425
|
+
async resolveWorkspaceAndBranch(token, instanceOrigin, flags, nonInteractive) {
|
|
426
|
+
let workspace;
|
|
427
|
+
let branch;
|
|
428
|
+
this.log('');
|
|
429
|
+
this.log('Fetching available workspaces...');
|
|
430
|
+
const workspaces = await this.fetchWorkspaces(token, instanceOrigin);
|
|
431
|
+
if (workspaces.length > 0) {
|
|
432
|
+
workspace = await this.resolveWorkspace(workspaces, flags.workspace, nonInteractive);
|
|
433
|
+
if (workspace) {
|
|
434
|
+
this.log('');
|
|
435
|
+
this.log('Fetching available branches...');
|
|
436
|
+
const branches = await this.fetchBranches(token, instanceOrigin, workspace.id);
|
|
437
|
+
branch = await this.resolveBranch(branches, flags.branch, nonInteractive);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
else if (flags.workspace) {
|
|
441
|
+
this.error(`Workspace '${flags.workspace}' not found: no workspaces are available on this instance.`);
|
|
442
|
+
}
|
|
443
|
+
if (flags.branch && !workspace) {
|
|
444
|
+
this.warn('Ignoring --branch: no workspace selected.');
|
|
445
|
+
}
|
|
446
|
+
return { branch, workspace };
|
|
447
|
+
}
|
|
281
448
|
async saveProfile(profile, configPath) {
|
|
282
449
|
const credentialsPath = resolveCredentialsPath(configPath);
|
|
283
450
|
const credDir = dirname(credentialsPath);
|
|
@@ -317,6 +484,7 @@ To authenticate, open the following URL in any browser:
|
|
|
317
484
|
noRefs: true,
|
|
318
485
|
});
|
|
319
486
|
fs.writeFileSync(credentialsPath, yamlContent, 'utf8');
|
|
487
|
+
return credentialsPath;
|
|
320
488
|
}
|
|
321
489
|
async selectBranch(branches) {
|
|
322
490
|
const { selectedBranch } = await inquirer.prompt([
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export default class EphemeralCreate extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
display: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
'expires-hours': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
};
|
|
17
|
+
run(): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
export default class EphemeralCreate extends BaseCommand {
|
|
4
|
+
static args = {
|
|
5
|
+
display: Args.string({
|
|
6
|
+
description: 'Display name for the ephemeral tenant',
|
|
7
|
+
required: true,
|
|
8
|
+
}),
|
|
9
|
+
};
|
|
10
|
+
static description = 'Create a new ephemeral (short-lived, auto-expiring) tenant in a workspace';
|
|
11
|
+
static examples = [
|
|
12
|
+
`$ xano ephemeral create "PR preview"
|
|
13
|
+
Created ephemeral tenant: PR preview (e4f2-9ab1-...) - ID: 42
|
|
14
|
+
Expires: in 1 hour
|
|
15
|
+
`,
|
|
16
|
+
`$ xano ephemeral create "Demo" --expires-hours 24 -w 114`,
|
|
17
|
+
`$ xano ephemeral create "Load test" --description "48h soak" --expires-hours 48 -o json`,
|
|
18
|
+
];
|
|
19
|
+
static flags = {
|
|
20
|
+
...BaseCommand.baseFlags,
|
|
21
|
+
description: Flags.string({
|
|
22
|
+
char: 'd',
|
|
23
|
+
description: 'Ephemeral tenant description',
|
|
24
|
+
required: false,
|
|
25
|
+
}),
|
|
26
|
+
'expires-hours': Flags.integer({
|
|
27
|
+
default: 1,
|
|
28
|
+
description: 'Hours until the tenant auto-expires (1-72)',
|
|
29
|
+
max: 72,
|
|
30
|
+
min: 1,
|
|
31
|
+
required: false,
|
|
32
|
+
}),
|
|
33
|
+
output: Flags.string({
|
|
34
|
+
char: 'o',
|
|
35
|
+
default: 'summary',
|
|
36
|
+
description: 'Output format',
|
|
37
|
+
options: ['summary', 'json'],
|
|
38
|
+
required: false,
|
|
39
|
+
}),
|
|
40
|
+
workspace: Flags.string({
|
|
41
|
+
char: 'w',
|
|
42
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
43
|
+
required: false,
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
46
|
+
async run() {
|
|
47
|
+
const { args, flags } = await this.parse(EphemeralCreate);
|
|
48
|
+
const { profile } = this.resolveProfile(flags);
|
|
49
|
+
// Ephemeral tenants REQUIRE a workspace (unlike sandbox, which is workspace-agnostic).
|
|
50
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
51
|
+
if (!workspaceId) {
|
|
52
|
+
this.error('No workspace ID provided. Ephemeral tenants require a workspace — use --workspace or set one in your profile.');
|
|
53
|
+
}
|
|
54
|
+
const body = {
|
|
55
|
+
display: args.display,
|
|
56
|
+
expires_hours: flags['expires-hours'],
|
|
57
|
+
tag: [],
|
|
58
|
+
};
|
|
59
|
+
if (flags.description)
|
|
60
|
+
body.description = flags.description;
|
|
61
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/ephemeral`;
|
|
62
|
+
try {
|
|
63
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
64
|
+
body: JSON.stringify(body),
|
|
65
|
+
headers: {
|
|
66
|
+
accept: 'application/json',
|
|
67
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
68
|
+
'Content-Type': 'application/json',
|
|
69
|
+
},
|
|
70
|
+
method: 'POST',
|
|
71
|
+
}, flags.verbose, profile.access_token);
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
const errorText = await response.text();
|
|
74
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
75
|
+
}
|
|
76
|
+
const tenant = (await response.json());
|
|
77
|
+
if (flags.output === 'json') {
|
|
78
|
+
this.log(JSON.stringify(tenant, null, 2));
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
this.log(`Created ephemeral tenant: ${tenant.display || tenant.name} (${tenant.name}) - ID: ${tenant.id}`);
|
|
82
|
+
if (tenant.state) {
|
|
83
|
+
this.log(` State: ${tenant.state}`);
|
|
84
|
+
}
|
|
85
|
+
if (tenant.ephemeral_expires_at) {
|
|
86
|
+
// The API serializes the timestamp field as a date string
|
|
87
|
+
// ("2026-07-24 19:49:15+0000"); tolerate a raw unix-epoch number too.
|
|
88
|
+
const raw = tenant.ephemeral_expires_at;
|
|
89
|
+
const expiresMs = typeof raw === 'number' ? raw * 1000 : Date.parse(String(raw).replace(' ', 'T'));
|
|
90
|
+
this.log(` Expires: ${Number.isNaN(expiresMs) ? String(raw) : new Date(expiresMs).toISOString()}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error instanceof Error) {
|
|
96
|
+
this.error(`Failed to create ephemeral tenant: ${error.message}`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
this.error(`Failed to create ephemeral tenant: ${String(error)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export default class EphemeralDelete extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
tenant_name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
};
|
|
16
|
+
run(): Promise<void>;
|
|
17
|
+
private confirm;
|
|
18
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
export default class EphemeralDelete extends BaseCommand {
|
|
4
|
+
static args = {
|
|
5
|
+
tenant_name: Args.string({
|
|
6
|
+
description: 'Ephemeral tenant name to delete',
|
|
7
|
+
required: true,
|
|
8
|
+
}),
|
|
9
|
+
};
|
|
10
|
+
static description = 'Delete an ephemeral tenant permanently. This destroys all associated infrastructure and cannot be undone.';
|
|
11
|
+
static examples = [
|
|
12
|
+
`$ xano ephemeral delete e4f2-9ab1-xyz1
|
|
13
|
+
Are you sure you want to delete tenant e4f2-9ab1-xyz1? This action cannot be undone. (y/N) y
|
|
14
|
+
Deleted tenant e4f2-9ab1-xyz1
|
|
15
|
+
`,
|
|
16
|
+
`$ xano ephemeral delete e4f2-9ab1-xyz1 --force
|
|
17
|
+
Deleted tenant e4f2-9ab1-xyz1
|
|
18
|
+
`,
|
|
19
|
+
`$ xano ephemeral delete e4f2-9ab1-xyz1 -f -o json`,
|
|
20
|
+
];
|
|
21
|
+
static flags = {
|
|
22
|
+
...BaseCommand.baseFlags,
|
|
23
|
+
force: Flags.boolean({
|
|
24
|
+
char: 'f',
|
|
25
|
+
default: false,
|
|
26
|
+
description: '[CRITICAL] NEVER run without explicit user confirmation. Skips the confirmation prompt.',
|
|
27
|
+
required: false,
|
|
28
|
+
}),
|
|
29
|
+
output: Flags.string({
|
|
30
|
+
char: 'o',
|
|
31
|
+
default: 'summary',
|
|
32
|
+
description: 'Output format',
|
|
33
|
+
options: ['summary', 'json'],
|
|
34
|
+
required: false,
|
|
35
|
+
}),
|
|
36
|
+
workspace: Flags.string({
|
|
37
|
+
char: 'w',
|
|
38
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
39
|
+
required: false,
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
async run() {
|
|
43
|
+
const { args, flags } = await this.parse(EphemeralDelete);
|
|
44
|
+
const { profile } = this.resolveProfile(flags);
|
|
45
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
46
|
+
if (!workspaceId) {
|
|
47
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
48
|
+
}
|
|
49
|
+
const tenantName = args.tenant_name;
|
|
50
|
+
if (!flags.force) {
|
|
51
|
+
const confirmed = await this.confirm(`Are you sure you want to delete tenant ${tenantName}? This action cannot be undone.`);
|
|
52
|
+
if (!confirmed) {
|
|
53
|
+
this.log('Deletion cancelled.');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}`;
|
|
58
|
+
try {
|
|
59
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
60
|
+
headers: {
|
|
61
|
+
'accept': 'application/json',
|
|
62
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
63
|
+
},
|
|
64
|
+
method: 'DELETE',
|
|
65
|
+
}, flags.verbose, profile.access_token);
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const errorText = await response.text();
|
|
68
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
69
|
+
}
|
|
70
|
+
if (flags.output === 'json') {
|
|
71
|
+
this.log(JSON.stringify({ deleted: true, tenant_name: tenantName }, null, 2));
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
this.log(`Deleted tenant ${tenantName}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error instanceof Error) {
|
|
79
|
+
this.error(`Failed to delete ephemeral tenant: ${error.message}`);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
this.error(`Failed to delete ephemeral tenant: ${String(error)}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async confirm(message) {
|
|
87
|
+
const readline = await import('node:readline');
|
|
88
|
+
const rl = readline.createInterface({
|
|
89
|
+
input: process.stdin,
|
|
90
|
+
output: process.stdout,
|
|
91
|
+
});
|
|
92
|
+
return new Promise((resolve) => {
|
|
93
|
+
rl.question(`${message} (y/N) `, (answer) => {
|
|
94
|
+
rl.close();
|
|
95
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export default class EphemeralEdit extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
tenant_name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
display: 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
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
};
|
|
17
|
+
run(): Promise<void>;
|
|
18
|
+
}
|